From 0598131553009b3d33092d8d3b589b2e60cc2e3d Mon Sep 17 00:00:00 2001 From: Greg Lamberson Date: Mon, 10 Aug 2026 19:04:21 -0500 Subject: [PATCH] feat(rdpeudp): add the RDP-UDP crate and the v1 handshake PDUs Starts `ironrdp-rdpeudp` with the structures [MS-RDPEUDP] section 2.2 defines for the three-way handshake that opens an RDP-UDP connection: the FEC header and its flags, the SYN data and extended SYN data payloads that negotiate sequence numbers, MTU and protocol version, the ACK vector with its run-length encoding, the AckOfAcks header and the correlation ID payload, plus the composite datagram that assembles them in the order 2.2.2 requires. Everything here is big-endian. Section 2.2 says "all of the messages written to the network or read from the network MUST be in network byte order", and the field diagrams number bits most significant first. The MS-RDPEUDP2 data transfer that a version 3 handshake leads to takes the opposite convention on both counts, which is worth keeping in mind when reading the two side by side. Two places where the spec is easy to read the wrong way, both decided against the captures in section 4 rather than against the prose: The ACK flag does not always announce an ACK vector. Section 2.2.2.1 defines it that way, but 3.1.5.1.3 builds the SYN+ACK as a plain SYN with the flag set and snSourceAck filled in, and the capture in 4.1.2 shows the SYNDATA payload following the header directly. So on a SYN the flag says only that snSourceAck is meaningful, and encode refuses a SYN that carries a vector rather than writing bytes a peer will misread. The ACK vector is padded to a DWORD boundary and its elements pack the state into the top two bits, both confirmed by the ACK packet capture in 4.2.3. No connection state machine yet; that follows, as does the v2 data transfer format. --- Cargo.lock | 11 + crates/ironrdp-rdpeudp/Cargo.toml | 37 ++ crates/ironrdp-rdpeudp/README.md | 20 + crates/ironrdp-rdpeudp/src/error.rs | 110 ++++ crates/ironrdp-rdpeudp/src/lib.rs | 10 + crates/ironrdp-rdpeudp/src/pdu/mod.rs | 265 ++++++++++ crates/ironrdp-rdpeudp/src/pdu/v1_ack.rs | 497 ++++++++++++++++++ crates/ironrdp-rdpeudp/src/pdu/v1_flags.rs | 106 ++++ crates/ironrdp-rdpeudp/src/pdu/v1_header.rs | 69 +++ crates/ironrdp-rdpeudp/src/pdu/v1_syn.rs | 299 +++++++++++ crates/ironrdp-testsuite-core/Cargo.toml | 1 + crates/ironrdp-testsuite-core/tests/main.rs | 1 + .../tests/rdpeudp/mod.rs | 3 + .../tests/rdpeudp/pdu_v1_datagram.rs | 444 ++++++++++++++++ .../tests/rdpeudp/pdu_v1_header.rs | 88 ++++ .../tests/rdpeudp/pdu_v1_syn.rs | 270 ++++++++++ 16 files changed, 2231 insertions(+) create mode 100644 crates/ironrdp-rdpeudp/Cargo.toml create mode 100644 crates/ironrdp-rdpeudp/README.md create mode 100644 crates/ironrdp-rdpeudp/src/error.rs create mode 100644 crates/ironrdp-rdpeudp/src/lib.rs create mode 100644 crates/ironrdp-rdpeudp/src/pdu/mod.rs create mode 100644 crates/ironrdp-rdpeudp/src/pdu/v1_ack.rs create mode 100644 crates/ironrdp-rdpeudp/src/pdu/v1_flags.rs create mode 100644 crates/ironrdp-rdpeudp/src/pdu/v1_header.rs create mode 100644 crates/ironrdp-rdpeudp/src/pdu/v1_syn.rs create mode 100644 crates/ironrdp-testsuite-core/tests/rdpeudp/mod.rs create mode 100644 crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_datagram.rs create mode 100644 crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_header.rs create mode 100644 crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_syn.rs diff --git a/Cargo.lock b/Cargo.lock index f73d47026..f24b8e6d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,6 +2883,16 @@ dependencies = [ "windows", ] +[[package]] +name = "ironrdp-rdpeudp" +version = "0.1.0" +dependencies = [ + "arbitrary", + "bitflags 2.13.1", + "ironrdp-core 0.2.1", + "ironrdp-error 0.2.0", +] + [[package]] name = "ironrdp-rdpeusb" version = "0.1.0" @@ -3042,6 +3052,7 @@ dependencies = [ "ironrdp-propertyset", "ironrdp-rdcleanpath", "ironrdp-rdpdr", + "ironrdp-rdpeudp", "ironrdp-rdpeusb", "ironrdp-rdpfile", "ironrdp-rdpsnd", diff --git a/crates/ironrdp-rdpeudp/Cargo.toml b/crates/ironrdp-rdpeudp/Cargo.toml new file mode 100644 index 000000000..133d5360c --- /dev/null +++ b/crates/ironrdp-rdpeudp/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ironrdp-rdpeudp" +version = "0.1.0" +readme = "README.md" +description = "RDP-UDP transport ([MS-RDPEUDP] and [MS-RDPEUDP2]) implementation for IronRDP" +edition.workspace = true +rust-version = "1.94" +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +# test = false # FIXME: turn off and keep tests in testsuite crates + +[features] +# Matches `ironrdp-pdu` and `ironrdp-rdpeusb`: `no_std` unless a consumer asks +# for `std`. The `alloc` crate is not optional here, because the variable-length +# payloads this protocol carries are heap-allocated, so there is no useful build +# without it and no `alloc` feature is offered. +default = [] +std = ["ironrdp-core/std", "ironrdp-error/std"] +# Structure-aware fuzzing support, mirroring `ironrdp-pdu`. +# `arbitrary` needs the standard library, so structure-aware fuzzing implies `std`. +arbitrary = ["dep:arbitrary", "std", "bitflags/arbitrary"] + +[dependencies] +arbitrary = { version = "1", features = ["derive"], optional = true } +ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public +ironrdp-error = { path = "../ironrdp-error", version = "0.2", features = ["alloc"] } # public +bitflags = "2.11" # public + +[lints] +workspace = true diff --git a/crates/ironrdp-rdpeudp/README.md b/crates/ironrdp-rdpeudp/README.md new file mode 100644 index 000000000..9b7340dd4 --- /dev/null +++ b/crates/ironrdp-rdpeudp/README.md @@ -0,0 +1,20 @@ +# IronRDP RDP-UDP + +Reliable UDP transport implemented as described in [MS-RDPEUDP] and +[MS-RDPEUDP2]. + +The two documents divide the work. [MS-RDPEUDP] defines the handshake that +opens a connection and negotiates a protocol version; from version 3 onward +that handshake leads into the data transfer defined by [MS-RDPEUDP2], which is +the one this crate implements. Note that the documents take opposite byte +orders, and number the bits in their diagrams in opposite directions. + +Sans-I/O: the state machine is driven by datagrams and by a caller-supplied +instant, and returns the datagrams it wants sent. It performs no I/O and reads +no clock, so it can be driven by any runtime. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP +[MS-RDPEUDP]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeudp/ +[MS-RDPEUDP2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeudp2/ diff --git a/crates/ironrdp-rdpeudp/src/error.rs b/crates/ironrdp-rdpeudp/src/error.rs new file mode 100644 index 000000000..127e36d4c --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/error.rs @@ -0,0 +1,110 @@ +//! Error types for the RDP-UDP connection state machine. +//! +//! These surface at the public API boundary of [`RdpeudpConnection`], and carry +//! both wire-level decode and encode failures from `ironrdp-core` and +//! protocol-level failures from the state machine itself. +//! +//! [`RdpeudpConnection`]: crate::RdpeudpConnection + +use core::fmt; + +use ironrdp_core::{DecodeError, EncodeError}; + +pub type RdpeudpResult = Result; + +pub type RdpeudpError = ironrdp_error::Error; + +#[non_exhaustive] +#[derive(Debug)] +pub enum RdpeudpErrorKind { + /// A datagram could not be decoded. + Decode(DecodeError), + + /// A datagram could not be encoded. + Encode(EncodeError), + + /// The connection is not in a state where this operation is meaningful. + /// + /// Sending before the handshake completes, or handling a datagram after the + /// connection is closed, both land here. + InvalidState, + + /// The send window is full. + /// + /// The caller should drain [`poll_transmit`] and wait for acknowledgements + /// to open the window before retrying. + /// + /// [`poll_transmit`]: crate::RdpeudpConnection::poll_transmit + SendBufferFull, + + /// The connection has been closed, locally or by the idle timeout. + ConnectionClosed, + + /// A datagram decoded cleanly but is not valid for the current state. + InvalidPacket { reason: &'static str }, +} + +impl fmt::Display for RdpeudpErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Decode(_) => write!(f, "decode error"), + Self::Encode(_) => write!(f, "encode error"), + Self::InvalidState => write!(f, "connection is in the wrong state for this operation"), + Self::SendBufferFull => write!(f, "send buffer is full"), + Self::ConnectionClosed => write!(f, "connection is closed"), + Self::InvalidPacket { reason } => write!(f, "invalid packet: {reason}"), + } + } +} + +#[cfg(feature = "std")] +impl core::error::Error for RdpeudpErrorKind { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Decode(error) => Some(error), + Self::Encode(error) => Some(error), + Self::InvalidState | Self::SendBufferFull | Self::ConnectionClosed | Self::InvalidPacket { .. } => None, + } + } +} + +pub trait RdpeudpErrorExt { + fn decode(error: DecodeError) -> Self; + fn encode(error: EncodeError) -> Self; + fn invalid_state(context: &'static str) -> Self; + fn send_buffer_full(context: &'static str) -> Self; + fn connection_closed(context: &'static str) -> Self; + fn invalid_packet(context: &'static str, reason: &'static str) -> Self; +} + +impl RdpeudpErrorExt for RdpeudpError { + #[track_caller] + fn decode(error: DecodeError) -> Self { + Self::new("decode error", RdpeudpErrorKind::Decode(error)) + } + + #[track_caller] + fn encode(error: EncodeError) -> Self { + Self::new("encode error", RdpeudpErrorKind::Encode(error)) + } + + #[track_caller] + fn invalid_state(context: &'static str) -> Self { + Self::new(context, RdpeudpErrorKind::InvalidState) + } + + #[track_caller] + fn send_buffer_full(context: &'static str) -> Self { + Self::new(context, RdpeudpErrorKind::SendBufferFull) + } + + #[track_caller] + fn connection_closed(context: &'static str) -> Self { + Self::new(context, RdpeudpErrorKind::ConnectionClosed) + } + + #[track_caller] + fn invalid_packet(context: &'static str, reason: &'static str) -> Self { + Self::new(context, RdpeudpErrorKind::InvalidPacket { reason }) + } +} diff --git a/crates/ironrdp-rdpeudp/src/lib.rs b/crates/ironrdp-rdpeudp/src/lib.rs new file mode 100644 index 000000000..39d44e329 --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/lib.rs @@ -0,0 +1,10 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +extern crate alloc; + +pub mod error; +pub mod pdu; + +pub use self::error::{RdpeudpError, RdpeudpErrorExt, RdpeudpErrorKind, RdpeudpResult}; diff --git a/crates/ironrdp-rdpeudp/src/pdu/mod.rs b/crates/ironrdp-rdpeudp/src/pdu/mod.rs new file mode 100644 index 000000000..99f36e8bc --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/pdu/mod.rs @@ -0,0 +1,265 @@ +//! RDPEUDP PDU definitions. +//! +//! V1 handshake format, MS-RDPEUDP section 2.2. +//! +//! These structures carry the three-way handshake (SYN, SYN+ACK, ACK) that +//! opens an RDP-UDP connection. They are used whatever protocol version the +//! endpoints settle on, because the version is what the handshake negotiates. +//! +//! Everything here is big-endian: section 2.2 says "all of the messages +//! written to the network or read from the network MUST be in network byte +//! order", and the field diagrams number bits most significant first. The +//! MS-RDPEUDP2 data transfer that a version 3 handshake leads to takes the +//! opposite convention on both counts. + +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +// ── V1 Handshake modules ── + +pub mod v1_ack; +pub mod v1_flags; +pub mod v1_header; +pub mod v1_syn; + +// ── V1 re-exports ── + +pub use v1_ack::{CorrelationIdPayload, V1AckOfAcksHeader, V1AckVectorElement, V1AckVectorHeader, VectorElementState}; +pub use v1_flags::V1Flags; +pub use v1_header::FecHeader; +pub use v1_syn::{MTU_MAX, MTU_MIN, SynDataExPayload, SynDataPayload, SynExFlags, UdpVersion}; + +// ════════════════════════════════════════════════════════════════════ +// Composite V1 Datagram +// ════════════════════════════════════════════════════════════════════ + +/// V1 flags that don't gate any optional payload; preserved on encode. +/// +/// DATA and FEC are payload-gating but have no corresponding fields +/// in V1Datagram (v1 data transfer is not supported). +const V1_STANDALONE_FLAGS: u16 = V1Flags::FIN.bits() + | V1Flags::CN.bits() + | V1Flags::CWR.bits() + | V1Flags::SACK_OPTION.bits() + | V1Flags::SYNLOSSY.bits() + | V1Flags::ACKDELAYED.bits(); + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// A complete v1 datagram (SYN, SYN+ACK, or ACK). +/// +/// MS-RDPEUDP Section 2.2. +/// The FecHeader's flags field determines which optional payloads +/// are present. On encode, payload-gating flags are automatically +/// derived from which `Option` fields are populated; standalone flags +/// (FIN, CN, CWR, SACK_OPTION, SYNLOSSY, ACKDELAYED) are preserved +/// from the header. +/// +/// Wire payload ordering (per MS-RDPEUDP Section 2.2.2): +/// 1. FecHeader (mandatory, 8 bytes) +/// 2. V1AckVectorHeader (if ACK flag and not SYN, see below) +/// 3. V1AckOfAcksHeader (if ACK_OF_ACKS flag) +/// 4. SynDataPayload (if SYN flag) +/// 5. CorrelationIdPayload (if CORRELATION_ID flag) +/// 6. SynDataExPayload (if SYNEX flag) +/// +/// A SYN+ACK is the exception to the ACK flag's usual meaning. Section +/// 2.2.2.1 defines the flag as "the ACK vector is present", but 3.1.5.1.3 +/// builds the SYN+ACK as a plain SYN with the ACK flag set and snSourceAck +/// filled in, and nothing else. The capture in section 4.1.2 confirms it: +/// uFlags is 0x0005 (SYN | ACK) and the SYNDATA payload follows the header +/// directly, with no ACK vector between them. So on a SYN+ACK the flag says +/// only that snSourceAck is meaningful, and `ack_vector` must be `None`. +/// +/// V1 data payloads (SOURCE_PAYLOAD / FEC_PAYLOAD) are not represented; +/// this crate always negotiates v2+ for data transfer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct V1Datagram { + /// Mandatory FecHeader. Standalone flags (FIN, CN, CWR, etc.) + /// are preserved; payload-gating flags are recomputed on encode. + pub header: FecHeader, + + /// ACK vector (run-length encoded receiver state). + /// Gated by `V1Flags::ACK`, except on a SYN+ACK, which sets that flag + /// without carrying a vector. Must be `None` whenever `syn_data` is set. + pub ack_vector: Option, + + /// AckOfAcks (resets ACK vector encoding base). + /// Gated by `V1Flags::ACK_OF_ACKS`. + pub ack_of_acks: Option, + + /// SYN data (ISN, MTU). + /// Gated by `V1Flags::SYN`. + pub syn_data: Option, + + /// Correlation ID for TCP/UDP binding. + /// Gated by `V1Flags::CORRELATION_ID`. + pub correlation_id: Option, + + /// Extended SYN data (version negotiation). + /// Gated by `V1Flags::SYNEX`. + pub syn_data_ex: Option, +} + +impl V1Datagram { + const NAME: &'static str = "V1 Datagram"; + + /// Compute the flags from populated fields, preserving standalone flags. + fn compute_flags(&self) -> V1Flags { + let mut flags = V1Flags::from_bits_truncate(self.header.flags.bits() & V1_STANDALONE_FLAGS); + + if self.ack_vector.is_some() { + flags |= V1Flags::ACK; + } + if self.ack_of_acks.is_some() { + flags |= V1Flags::ACK_OF_ACKS; + } + if self.syn_data.is_some() { + flags |= V1Flags::SYN; + // On a SYN+ACK the ACK flag has no payload to be derived from, + // so take it from the caller. Section 3.1.5.1.3 requires it to + // be set on the server's half of the handshake. + flags |= self.header.flags & V1Flags::ACK; + } + if self.correlation_id.is_some() { + flags |= V1Flags::CORRELATION_ID; + } + if self.syn_data_ex.is_some() { + flags |= V1Flags::SYNEX; + } + + flags + } +} + +impl Encode for V1Datagram { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + // A SYN+ACK carries no ACK vector (3.1.5.1.3, capture in 4.1.2), and + // a receiver keying off the SYN flag would read whatever we wrote here + // as the start of the SYNDATA payload. + if self.syn_data.is_some() && self.ack_vector.is_some() { + return Err(ironrdp_core::invalid_field_err!( + Self::NAME, + "ack_vector", + "a SYN datagram cannot carry an ACK vector" + )); + } + + ironrdp_core::ensure_size!(in: dst, size: self.size()); + + // Write header with auto-computed flags + let header = FecHeader { + flags: self.compute_flags(), + ..self.header + }; + header.encode(dst)?; + + // Write payloads in spec-mandated order (MS-RDPEUDP Section 2.2.2) + if let Some(ref ack_vector) = self.ack_vector { + ack_vector.encode(dst)?; + } + if let Some(ref ack_of_acks) = self.ack_of_acks { + ack_of_acks.encode(dst)?; + } + if let Some(ref syn_data) = self.syn_data { + syn_data.encode(dst)?; + } + if let Some(ref correlation_id) = self.correlation_id { + correlation_id.encode(dst)?; + } + if let Some(ref syn_data_ex) = self.syn_data_ex { + syn_data_ex.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let mut total = self.header.size(); + if let Some(ref av) = self.ack_vector { + total += av.size(); + } + if let Some(ref aoa) = self.ack_of_acks { + total += aoa.size(); + } + if let Some(ref sd) = self.syn_data { + total += sd.size(); + } + if let Some(ref cid) = self.correlation_id { + total += cid.size(); + } + if let Some(ref sdex) = self.syn_data_ex { + total += sdex.size(); + } + total + } +} + +impl Decode<'_> for V1Datagram { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = FecHeader::decode(src)?; + + // V1 data payloads are not supported; reject if present since we + // cannot skip them without knowing their wire size. + if header.flags.contains(V1Flags::DATA) { + return Err(ironrdp_core::invalid_field_err!( + "V1 Datagram", + "flags", + "DATA flag is not supported in handshake datagrams" + )); + } + if header.flags.contains(V1Flags::FEC) { + return Err(ironrdp_core::invalid_field_err!( + "V1 Datagram", + "flags", + "FEC flag is not supported in handshake datagrams" + )); + } + + // Decode payloads in spec-mandated order, gated by flags. + // + // SYN suppresses the ACK vector: on a SYN+ACK the ACK flag marks + // snSourceAck as meaningful rather than announcing a vector. See the + // note on `V1Datagram`. + let ack_vector = if header.flags.contains(V1Flags::ACK) && !header.flags.contains(V1Flags::SYN) { + Some(V1AckVectorHeader::decode(src)?) + } else { + None + }; + + let ack_of_acks = if header.flags.contains(V1Flags::ACK_OF_ACKS) { + Some(V1AckOfAcksHeader::decode(src)?) + } else { + None + }; + + let syn_data = if header.flags.contains(V1Flags::SYN) { + Some(SynDataPayload::decode(src)?) + } else { + None + }; + + let correlation_id = if header.flags.contains(V1Flags::CORRELATION_ID) { + Some(CorrelationIdPayload::decode(src)?) + } else { + None + }; + + let syn_data_ex = if header.flags.contains(V1Flags::SYNEX) { + Some(SynDataExPayload::decode(src)?) + } else { + None + }; + + Ok(Self { + header, + ack_vector, + ack_of_acks, + syn_data, + correlation_id, + syn_data_ex, + }) + } +} diff --git a/crates/ironrdp-rdpeudp/src/pdu/v1_ack.rs b/crates/ironrdp-rdpeudp/src/pdu/v1_ack.rs new file mode 100644 index 000000000..4241da3eb --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/pdu/v1_ack.rs @@ -0,0 +1,497 @@ +//! V1 ACK-related structures. +//! +//! `RDPUDP_ACK_VECTOR_HEADER` (MS-RDPEUDP Section 2.2.2.7) and +//! `RDPUDP_ACK_OF_ACKVECTOR_HEADER` (MS-RDPEUDP Section 2.2.2.6). + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +// -- ACK Vector Element -- + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// State of a run of datagrams in the ACK vector. +/// +/// [MS-RDPEUDP] 2.2.1.1. The two reserved values are defined by the +/// specification and unused by it, so they are represented rather than +/// rejected: a peer that sends one is speaking the protocol as written. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VectorElementState { + /// The datagrams in this run were received. + DatagramReceived, + /// Reserved by the specification, not used. + Reserved1, + /// Reserved by the specification, not used. + Reserved2, + /// The datagrams in this run have not been received yet. + DatagramNotYetReceived, +} + +impl VectorElementState { + fn to_bits(self) -> u8 { + match self { + Self::DatagramReceived => 0, + Self::Reserved1 => 1, + Self::Reserved2 => 2, + Self::DatagramNotYetReceived => 3, + } + } + + fn from_bits(bits: u8) -> Self { + match bits & 0x03 { + 0 => Self::DatagramReceived, + 1 => Self::Reserved1, + 2 => Self::Reserved2, + _ => Self::DatagramNotYetReceived, + } + } + + /// Whether this state means the datagrams arrived. + #[must_use] + pub fn is_received(self) -> bool { + matches!(self, Self::DatagramReceived) + } +} + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// One run in the ACK vector. +/// +/// [MS-RDPEUDP] 2.2.2.7.1: the two most significant bits carry the +/// [`VectorElementState`] and the six least significant bits carry the length +/// of the run. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct V1AckVectorElement { + /// State shared by every datagram in this run. + pub state: VectorElementState, + + /// Number of consecutive datagrams in this state (0..=63). + pub length: u8, +} + +impl V1AckVectorElement { + /// Longest run a single element can express: the length field is six bits. + pub const MAX_LENGTH: u8 = 0x3F; + + /// Encode to a single byte: state in bits 6 and 7, run length in bits 0 + /// through 5. + fn to_byte(self) -> u8 { + (self.state.to_bits() << 6) | (self.length & Self::MAX_LENGTH) + } + + /// Decode from a single byte. + fn from_byte(byte: u8) -> Self { + Self { + state: VectorElementState::from_bits(byte >> 6), + length: byte & Self::MAX_LENGTH, + } + } +} + +// -- RDPUDP_ACK_VECTOR_HEADER -- + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// ACK vector describing the state of the receiver's packet queue. +/// +/// [MS-RDPEUDP] 2.2.2.7. +/// Wire layout: `uAckVectorSize(2)` + `AckVectorElement[](variable)` + +/// `Padding(variable)`. +/// +/// The elements are run-length encoded, each carrying a state and the number +/// of consecutive datagrams sharing it. The structure is then padded so it +/// ends on a DWORD boundary, which 2.2.2.7 requires and the section 4.2.1 +/// capture shows (`00 01 04 00`: two size bytes, one element, one pad byte). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct V1AckVectorHeader { + /// RLE-encoded state of packets in the receiver queue. + pub elements: Vec, +} + +impl V1AckVectorHeader { + const FIXED_PART_SIZE: usize = 2; // uAckVectorSize + const NAME: &'static str = "RDPUDP_ACK_VECTOR_HEADER"; + + /// Bytes of padding needed so the structure ends on a DWORD boundary. + fn padding_len(element_count: usize) -> usize { + (Self::FIXED_PART_SIZE + element_count).next_multiple_of(4) - Self::FIXED_PART_SIZE - element_count + } +} + +impl Encode for V1AckVectorHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let count: u16 = ironrdp_core::cast_length!("RDPUDP_ACK_VECTOR_HEADER", "uAckVectorSize", self.elements.len())?; + + ironrdp_core::ensure_size!(in: dst, size: self.size()); + + dst.write_u16_be(count); + for element in &self.elements { + dst.write_u8(element.to_byte()); + } + ironrdp_core::write_padding!(dst, Self::padding_len(self.elements.len())); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.elements.len() + Self::padding_len(self.elements.len()) + } +} + +impl Decode<'_> for V1AckVectorHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_core::ensure_fixed_part_size!(in: src); + + let count = src.read_u16_be(); + let count_usize = usize::from(count); + + // The padding is part of the structure, so it has to be present before + // we start consuming: ensuring only the elements would let a truncated + // datagram advance the cursor past the end while skipping it. + let padding = Self::padding_len(count_usize); + ironrdp_core::ensure_size!(in: src, size: count_usize + padding); + + let mut elements = Vec::with_capacity(count_usize); + for _ in 0..count_usize { + elements.push(V1AckVectorElement::from_byte(src.read_u8())); + } + ironrdp_core::read_padding!(src, padding); + + Ok(Self { elements }) + } +} + +// -- RDPUDP_ACK_OF_ACKVECTOR_HEADER -- + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// Resets the starting position of ACK vector encoding at the receiver. +/// +/// MS-RDPEUDP Section 2.2.2.6. +/// Wire layout: `snResetSeqNum(4)` = 4 bytes. +/// +/// Sent after approximately every 20 packets. The receiver generates +/// ACK vectors only for sequence numbers greater than `reset_seq_num`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct V1AckOfAcksHeader { + /// Sequence number to reset the ACK vector base to. + /// The sender populates this with the greatest cumulative ACK + /// it has received and processed. + pub reset_seq_num: u32, +} + +impl V1AckOfAcksHeader { + const FIXED_PART_SIZE: usize = 4; + const NAME: &'static str = "RDPUDP_ACK_OF_ACKVECTOR_HEADER"; +} + +impl Encode for V1AckOfAcksHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ironrdp_core::ensure_fixed_part_size!(in: dst); + dst.write_u32_be(self.reset_seq_num); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for V1AckOfAcksHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_core::ensure_fixed_part_size!(in: src); + let reset_seq_num = src.read_u32_be(); + Ok(Self { reset_seq_num }) + } +} + +// -- RDPUDP_CORRELATION_ID_PAYLOAD -- + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// Correlation ID for diagnostic binding between TCP and UDP connections. +/// +/// [MS-RDPEUDP] 2.2.2.8. +/// Wire layout: `uCorrelationId(16)` + `uReserved(16)` = 32 bytes. +/// +/// `uReserved` is sixteen zero bytes and is not represented as a field: 3.1.5.1.1 +/// requires it to be all zeros on send, so there is nothing for a caller to +/// choose. It is written and skipped by the codec. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CorrelationIdPayload { + /// A 16-byte identifier that correlates the UDP connection + /// with its parent TCP connection for diagnostic purposes. + pub correlation_id: [u8; 16], +} + +impl CorrelationIdPayload { + const CORRELATION_ID_SIZE: usize = 16; + const RESERVED_SIZE: usize = 16; + const FIXED_PART_SIZE: usize = Self::CORRELATION_ID_SIZE + Self::RESERVED_SIZE; + const NAME: &'static str = "RDPUDP_CORRELATION_ID_PAYLOAD"; +} + +impl Encode for CorrelationIdPayload { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ironrdp_core::ensure_fixed_part_size!(in: dst); + dst.write_slice(&self.correlation_id); + ironrdp_core::write_padding!(dst, Self::RESERVED_SIZE); + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for CorrelationIdPayload { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_core::ensure_fixed_part_size!(in: src); + let correlation_id = src.read_array::<{ Self::CORRELATION_ID_SIZE }>(); + ironrdp_core::read_padding!(src, Self::RESERVED_SIZE); + Ok(Self { correlation_id }) + } +} + +#[cfg(test)] +mod tests { + use ironrdp_core::{decode, encode_vec}; + + use super::*; + + // -- V1AckVectorElement tests -- + // + // Byte layout is [MS-RDPEUDP] 2.2.2.7.1: state in bits 6 and 7, run length + // in bits 0 through 5. + + #[test] + fn ack_vector_element_received() { + let elem = V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 42, + }; + + // DATAGRAM_RECEIVED is 0, so a received run is just its length. + assert_eq!(elem.to_byte(), 42); + assert_eq!(V1AckVectorElement::from_byte(42), elem); + } + + #[test] + fn ack_vector_element_not_received() { + let elem = V1AckVectorElement { + state: VectorElementState::DatagramNotYetReceived, + length: 3, + }; + + // DATAGRAM_NOT_YET_RECEIVED is 3, which lands in bits 6 and 7. + assert_eq!(elem.to_byte(), 0xC3); + assert_eq!(V1AckVectorElement::from_byte(0xC3), elem); + } + + #[test] + fn ack_vector_element_reserved_states_survive_a_round_trip() { + for (state, bits) in [ + (VectorElementState::Reserved1, 0x40), + (VectorElementState::Reserved2, 0x80), + ] { + let elem = V1AckVectorElement { state, length: 5 }; + assert_eq!(elem.to_byte(), bits | 5); + assert_eq!(V1AckVectorElement::from_byte(bits | 5), elem); + } + } + + #[test] + fn ack_vector_element_max_length() { + let elem = V1AckVectorElement { + state: VectorElementState::DatagramNotYetReceived, + length: V1AckVectorElement::MAX_LENGTH, + }; + + assert_eq!(elem.to_byte(), 0xFF); + assert_eq!(V1AckVectorElement::from_byte(0xFF), elem); + } + + /// The run length is six bits, so a longer run cannot be expressed and + /// must not silently overflow into the state bits. + #[test] + fn ack_vector_element_length_is_masked_not_overflowed() { + let elem = V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 64, + }; + + assert_eq!(elem.to_byte() >> 6, 0, "length overflowed into the state bits"); + } + + // -- V1AckVectorHeader tests -- + + #[test] + fn encode_ack_vector_empty() { + let header = V1AckVectorHeader { elements: Vec::new() }; + let encoded = encode_vec(&header).expect("encode"); + + // Two size bytes are not on a DWORD boundary, so two pad bytes follow. + assert_eq!(encoded.as_slice(), &[0x00, 0x00, 0x00, 0x00]); + assert_eq!(header.size(), 4); + } + + #[test] + fn encode_ack_vector_with_elements() { + let header = V1AckVectorHeader { + elements: vec![ + V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 10, + }, + V1AckVectorElement { + state: VectorElementState::DatagramNotYetReceived, + length: 2, + }, + V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 5, + }, + ], + }; + + let encoded = encode_vec(&header).expect("encode"); + assert_eq!( + encoded.as_slice(), + &[ + 0x00, 0x03, // uAckVectorSize = 3, network byte order + 10, // DATAGRAM_RECEIVED, run of 10 + 0xC2, // DATAGRAM_NOT_YET_RECEIVED, run of 2 + 5, // DATAGRAM_RECEIVED, run of 5 + 0x00, 0x00, 0x00, // padding to a DWORD boundary + ] + ); + assert_eq!(header.size(), 8); + } + + /// The section 4.2.1 capture, which is the authority for both the byte + /// order and the padding. + #[test] + fn decode_ack_vector_from_the_spec_capture() { + // From [MS-RDPEUDP] 4.2.1: uAckVectorSize 0x0001, one element 0x04, + // then one pad byte. + let bytes = [0x00, 0x01, 0x04, 0x00]; + + let header: V1AckVectorHeader = decode(&bytes).expect("decode"); + assert_eq!( + header.elements, + vec![V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 4, + }], + "the capture documents element 0x04 as DATAGRAM_RECEIVED with a run of 4" + ); + + assert_eq!(encode_vec(&header).expect("encode"), bytes); + } + + #[test] + fn ack_vector_roundtrip() { + for count in [0usize, 1, 2, 3, 4, 5, 9] { + let original = V1AckVectorHeader { + elements: (0..count) + .map(|i| V1AckVectorElement { + state: if i % 2 == 0 { + VectorElementState::DatagramReceived + } else { + VectorElementState::DatagramNotYetReceived + }, + length: V1AckVectorElement::MAX_LENGTH, + }) + .collect(), + }; + + let encoded = encode_vec(&original).expect("encode"); + assert_eq!(encoded.len() % 4, 0, "structure must end on a DWORD boundary"); + assert_eq!(decode::(&encoded).expect("decode"), original); + } + } + + #[test] + fn ack_vector_insufficient_bytes_for_count() { + let bytes = [0x01]; // only 1 byte, need 2 for the count + let result: DecodeResult = decode(&bytes); + assert!(result.is_err()); + } + + #[test] + fn ack_vector_insufficient_bytes_for_elements() { + let bytes = [0x00, 0x05, 0x80]; // claims 5 elements, carries 1 + let result: DecodeResult = decode(&bytes); + assert!(result.is_err()); + } + + // -- V1AckOfAcksHeader tests -- + + #[test] + fn ack_of_acks_roundtrip() { + let original = V1AckOfAcksHeader { + reset_seq_num: 0xDEAD_BEEF, + }; + + let encoded = encode_vec(&original).expect("encode"); + assert_eq!( + encoded.as_slice(), + &[0xDE, 0xAD, 0xBE, 0xEF], + "[MS-RDPEUDP] 2.2 requires network byte order" + ); + + assert_eq!(decode::(&encoded).expect("decode"), original); + } + + #[test] + fn ack_of_acks_size() { + let header = V1AckOfAcksHeader { reset_seq_num: 0 }; + assert_eq!(header.size(), 4); + } + + // -- CorrelationIdPayload tests -- + + #[test] + fn correlation_id_roundtrip() { + let original = CorrelationIdPayload { + correlation_id: [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + ], + }; + + let encoded = encode_vec(&original).expect("encode"); + assert_eq!(encoded.len(), 32, "uCorrelationId(16) + uReserved(16)"); + assert_eq!(&encoded[16..], &[0u8; 16], "uReserved must be zeros"); + + assert_eq!(decode::(&encoded).expect("decode"), original); + } + + /// The correlation identifier from the section 4.1.1 SYN capture. + #[test] + fn correlation_id_from_the_spec_capture() { + let mut bytes = [0u8; 32]; + let id = [ + 0xD2, 0x35, 0xAC, 0x43, 0x89, 0x41, 0x42, 0xDA, 0xB1, 0x0E, 0xDD, 0x68, 0x87, 0xF7, 0xF9, 0xFB, + ]; + bytes[..16].copy_from_slice(&id); + + let payload: CorrelationIdPayload = decode(&bytes).expect("decode"); + assert_eq!(payload.correlation_id, id); + assert_eq!(encode_vec(&payload).expect("encode"), bytes); + } + + #[test] + fn correlation_id_insufficient_bytes() { + let bytes = [0x01, 0x02, 0x03]; // only 3 bytes, need 32 + let result: DecodeResult = decode(&bytes); + assert!(result.is_err()); + } +} diff --git a/crates/ironrdp-rdpeudp/src/pdu/v1_flags.rs b/crates/ironrdp-rdpeudp/src/pdu/v1_flags.rs new file mode 100644 index 000000000..b9cc20f23 --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/pdu/v1_flags.rs @@ -0,0 +1,106 @@ +//! V1 header flags per MS-RDPEUDP Section 2.2.2.1. +//! +//! The `uFlags` field in `RDPUDP_FEC_HEADER` is a 16-bit bitmap +//! indicating which optional payloads are present and which +//! protocol features are active. + +use bitflags::bitflags; + +bitflags! { + /// Flags in the RDPUDP_FEC_HEADER `uFlags` field. + /// + /// MS-RDPEUDP Section 2.2.2.1. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + pub struct V1Flags: u16 { + /// SYN: connection initialization. + const SYN = 0x0001; + + /// FIN: connection teardown (currently unused by the spec). + const FIN = 0x0002; + + /// ACK: `RDPUDP_ACK_VECTOR_HEADER` is present, except on a SYN+ACK + /// (section 3.1.5.1.3), where it marks snSourceAck as meaningful and + /// no vector follows. + const ACK = 0x0004; + + /// DATA: `RDPUDP_SOURCE_PAYLOAD_HEADER` or + /// `RDPUDP_FEC_PAYLOAD_HEADER` follows. + const DATA = 0x0008; + + /// FEC: `RDPUDP_FEC_PAYLOAD_HEADER` is present. + const FEC = 0x0010; + + /// Congestion Notification: receiver detected packet loss. + const CN = 0x0020; + + /// Congestion Window Reset: sender reacted to CN. + const CWR = 0x0040; + + /// SACK option (not used). + const SACK_OPTION = 0x0080; + + /// ACK-of-ACKs: `RDPUDP_ACK_OF_ACKVECTOR_HEADER` is present. + const ACK_OF_ACKS = 0x0100; + + /// Connection does not require persistent retransmits (lossy mode). + const SYNLOSSY = 0x0200; + + /// Receiver delayed generating this ACK; do not use for RTT estimation. + const ACKDELAYED = 0x0400; + + /// `RDPUDP_CORRELATION_ID_PAYLOAD` is present. + const CORRELATION_ID = 0x0800; + + /// `RDPUDP_SYNDATAEX_PAYLOAD` is present. + const SYNEX = 0x1000; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flag_values_match_spec() { + assert_eq!(V1Flags::SYN.bits(), 0x0001); + assert_eq!(V1Flags::FIN.bits(), 0x0002); + assert_eq!(V1Flags::ACK.bits(), 0x0004); + assert_eq!(V1Flags::DATA.bits(), 0x0008); + assert_eq!(V1Flags::FEC.bits(), 0x0010); + assert_eq!(V1Flags::CN.bits(), 0x0020); + assert_eq!(V1Flags::CWR.bits(), 0x0040); + assert_eq!(V1Flags::SACK_OPTION.bits(), 0x0080); + assert_eq!(V1Flags::ACK_OF_ACKS.bits(), 0x0100); + assert_eq!(V1Flags::SYNLOSSY.bits(), 0x0200); + assert_eq!(V1Flags::ACKDELAYED.bits(), 0x0400); + assert_eq!(V1Flags::CORRELATION_ID.bits(), 0x0800); + assert_eq!(V1Flags::SYNEX.bits(), 0x1000); + } + + #[test] + fn syn_datagram_flags() { + // A typical SYN datagram sets SYN + SYNEX + let flags = V1Flags::SYN | V1Flags::SYNEX; + assert!(flags.contains(V1Flags::SYN)); + assert!(flags.contains(V1Flags::SYNEX)); + assert!(!flags.contains(V1Flags::ACK)); + } + + #[test] + fn syn_ack_datagram_flags() { + // SYN+ACK sets SYN + ACK + SYNEX + let flags = V1Flags::SYN | V1Flags::ACK | V1Flags::SYNEX; + assert!(flags.contains(V1Flags::SYN)); + assert!(flags.contains(V1Flags::ACK)); + assert!(flags.contains(V1Flags::SYNEX)); + } + + #[test] + fn roundtrip_from_bits() { + let original = V1Flags::SYN | V1Flags::CN | V1Flags::CORRELATION_ID; + let bits = original.bits(); + let restored = V1Flags::from_bits_truncate(bits); + assert_eq!(original, restored); + } +} diff --git a/crates/ironrdp-rdpeudp/src/pdu/v1_header.rs b/crates/ironrdp-rdpeudp/src/pdu/v1_header.rs new file mode 100644 index 000000000..de61325df --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/pdu/v1_header.rs @@ -0,0 +1,69 @@ +//! RDPUDP_FEC_HEADER: the mandatory header for every v1 datagram. +//! +//! MS-RDPEUDP Section 2.2.2.1. +//! Wire layout: `snSourceAck(4)` + `uReceiveWindowSize(2)` + `uFlags(2)` = 8 bytes, little-endian. + +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +use super::v1_flags::V1Flags; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// Common header present on every v1 datagram. +/// +/// MS-RDPEUDP Section 2.2.2.1. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FecHeader { + /// Highest sequence number the remote endpoint has received. + /// Set to `0xFFFFFFFF` in the initial SYN. + pub sn_source_ack: u32, + + /// Size of the receiver's buffer (in packets). + pub receive_window_size: u16, + + /// Bitmap of `V1Flags` indicating optional payloads and features. + pub flags: V1Flags, +} + +impl FecHeader { + const FIXED_PART_SIZE: usize = 4 + 2 + 2; // snSourceAck + uReceiveWindowSize + uFlags + + const NAME: &'static str = "RDPUDP_FEC_HEADER"; +} + +impl Encode for FecHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ironrdp_core::ensure_fixed_part_size!(in: dst); + + dst.write_u32_be(self.sn_source_ack); + dst.write_u16_be(self.receive_window_size); + dst.write_u16_be(self.flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for FecHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_core::ensure_fixed_part_size!(in: src); + + let sn_source_ack = src.read_u32_be(); + let receive_window_size = src.read_u16_be(); + let flags_raw = src.read_u16_be(); + + let flags = V1Flags::from_bits_truncate(flags_raw); + + Ok(Self { + sn_source_ack, + receive_window_size, + flags, + }) + } +} diff --git a/crates/ironrdp-rdpeudp/src/pdu/v1_syn.rs b/crates/ironrdp-rdpeudp/src/pdu/v1_syn.rs new file mode 100644 index 000000000..372d36bd9 --- /dev/null +++ b/crates/ironrdp-rdpeudp/src/pdu/v1_syn.rs @@ -0,0 +1,299 @@ +//! SYN-related payloads for the v1 three-way handshake. +//! +//! `RDPUDP_SYNDATA_PAYLOAD` (MS-RDPEUDP Section 2.2.2.5) and +//! `RDPUDP_SYNDATAEX_PAYLOAD` (MS-RDPEUDP Section 2.2.2.9). + +use bitflags::bitflags; +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +// -- MTU constants per MS-RDPEUDP Section 2.2.2.5 -- + +/// Minimum allowed MTU value. +pub const MTU_MIN: u16 = 1132; + +/// Maximum allowed MTU value. +pub const MTU_MAX: u16 = 1232; + +// -- RDPUDP_SYNDATA_PAYLOAD -- + +/// Connection initialization parameters exchanged in SYN and SYN+ACK datagrams. +/// +/// MS-RDPEUDP Section 2.2.2.5. +/// Wire layout: `snInitialSequenceNumber(4)` + `uUpStreamMtu(2)` + `uDownStreamMtu(2)` = 8 bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SynDataPayload { + /// Starting sequence number (random, analogous to TCP ISN per RFC 1948). + pub initial_sequence_number: u32, + + /// Maximum datagram size this endpoint can generate. + /// Must be in `1132..=1232`. + pub upstream_mtu: u16, + + /// Maximum datagram size this endpoint can accept. + /// Must be in `1132..=1232`. + pub downstream_mtu: u16, +} + +/// Generates MTUs inside the range the decoder accepts. +/// +/// The derived implementation would draw `uUpStreamMtu` and `uDownStreamMtu` +/// from the whole of `u16`, and 2.2.2.5 constrains both to 1132..=1232. Almost +/// every generated SYN would then be rejected by its own decoder, so the fuzzer +/// would spend its budget on the validation branch instead of reaching the +/// handshake behind it. +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for SynDataPayload { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Self { + initial_sequence_number: u32::arbitrary(u)?, + upstream_mtu: u.int_in_range(MTU_MIN..=MTU_MAX)?, + downstream_mtu: u.int_in_range(MTU_MIN..=MTU_MAX)?, + }) + } +} + +impl SynDataPayload { + const FIXED_PART_SIZE: usize = 4 + 2 + 2; + const NAME: &'static str = "RDPUDP_SYNDATA_PAYLOAD"; + + fn validate_mtu(value: u16, field: &'static str) -> DecodeResult<()> { + if !(MTU_MIN..=MTU_MAX).contains(&value) { + return Err(ironrdp_core::invalid_field_err!( + "RDPUDP_SYNDATA_PAYLOAD", + field, + "mtu must be in 1132..=1232" + )); + } + Ok(()) + } +} + +impl Encode for SynDataPayload { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ironrdp_core::ensure_fixed_part_size!(in: dst); + + dst.write_u32_be(self.initial_sequence_number); + dst.write_u16_be(self.upstream_mtu); + dst.write_u16_be(self.downstream_mtu); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for SynDataPayload { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_core::ensure_fixed_part_size!(in: src); + + let initial_sequence_number = src.read_u32_be(); + let upstream_mtu = src.read_u16_be(); + let downstream_mtu = src.read_u16_be(); + + Self::validate_mtu(upstream_mtu, "uUpStreamMtu")?; + Self::validate_mtu(downstream_mtu, "uDownStreamMtu")?; + + Ok(Self { + initial_sequence_number, + upstream_mtu, + downstream_mtu, + }) + } +} + +// -- SYNDATAEX flags -- + +bitflags! { + /// Flags in the RDPUDP_SYNDATAEX_PAYLOAD `uSynExFlags` field. + /// + /// MS-RDPEUDP Section 2.2.2.9. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + pub struct SynExFlags: u16 { + /// The `uUdpVer` field indicates a supported protocol version. + const VERSION_INFO_VALID = 0x0001; + } +} + +// -- UDP protocol versions -- + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// RDPEUDP protocol version negotiated via SYNDATAEX. +/// +/// MS-RDPEUDP Section 2.2.2.9. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u16)] +pub enum UdpVersion { + /// v1: min retransmit 500ms, min ACK delay 200ms. + /// Data transfer uses the MS-RDPEUDP wire format. + V1 = 0x0001, + /// v2: min retransmit 300ms, min ACK delay 50ms. + /// Data transfer still uses the MS-RDPEUDP wire format. + V2 = 0x0002, + /// v3: data transfer uses the MS-RDPEUDP2 wire format. + /// The client's SYN carries a cookieHash binding it to the + /// multitransport request. + V3 = 0x0101, +} + +impl UdpVersion { + /// Try to parse a version from its wire representation. + pub fn from_u16(value: u16) -> Option { + match value { + 0x0001 => Some(Self::V1), + 0x0002 => Some(Self::V2), + 0x0101 => Some(Self::V3), + _ => None, + } + } + + /// Returns the wire representation. + pub fn as_u16(self) -> u16 { + match self { + Self::V1 => 0x0001, + Self::V2 => 0x0002, + Self::V3 => 0x0101, + } + } + + /// Whether this version selects the MS-RDPEUDP2 wire format for data + /// transfer. + /// + /// Only version 3 does. The name of version 2 invites the opposite + /// reading and it is wrong: 2.2.2.9 describes 0x0002 purely as a pair of + /// shorter timeouts, and 0x0101 is the only row that mentions + /// [MS-RDPEUDP2]. 1.3.2.2 states it from the other direction, that the + /// MS-RDPEUDP data transfer messages "MUST be used only when the version + /// negotiated in the UDP connection initialization phase is version 1 or + /// version 2". + pub fn uses_v2_wire_format(self) -> bool { + matches!(self, Self::V3) + } + + /// Minimum retransmission timeout for this version. + pub fn min_retransmit_ms(self) -> u32 { + match self { + Self::V1 => 500, + Self::V2 | Self::V3 => 300, + } + } + + /// Minimum delayed ACK timeout for this version. + pub fn min_ack_delay_ms(self) -> u32 { + match self { + Self::V1 => 200, + Self::V2 | Self::V3 => 50, + } + } +} + +// -- RDPUDP_SYNDATAEX_PAYLOAD -- + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// Extended SYN parameters for version negotiation. +/// +/// MS-RDPEUDP Section 2.2.2.9. +/// Wire layout: `uSynExFlags(2)` + `uUdpVer(2)` + optional `cookieHash(32)`. +/// +/// `cookieHash` is present only in a client→server SYN with v3 (`RDPUDP_PROTOCOL_VERSION_3`). +/// It contains the SHA-256 hash of the server's `securityCookie` from the +/// Initiate Multitransport Request PDU (MS-RDPBCGR Section 2.2.15.1). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SynDataExPayload { + /// Extended flags (must include `VERSION_INFO_VALID` for version negotiation). + pub syn_ex_flags: SynExFlags, + + /// Protocol version advertised by this endpoint. + pub udp_ver: UdpVersion, + + /// SHA-256 hash of securityCookie. Present only for v3 client→server SYN. + /// Encoded as 8 × 4-byte big-endian unsigned integers. + pub cookie_hash: Option<[u8; 32]>, +} + +impl SynDataExPayload { + const FIXED_PART_SIZE: usize = 2 + 2; // uSynExFlags + uUdpVer + const COOKIE_HASH_SIZE: usize = 32; + const NAME: &'static str = "RDPUDP_SYNDATAEX_PAYLOAD"; + + /// Total encoded size including optional cookie hash. + fn encoded_size(&self) -> usize { + let base = Self::FIXED_PART_SIZE; + if self.cookie_hash.is_some() { + base + Self::COOKIE_HASH_SIZE + } else { + base + } + } +} + +impl Encode for SynDataExPayload { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + // The decoder reads the cookie hash only for version 3, so a hash + // carried alongside any other version would be written and never read + // back. Reject it rather than drop it silently: a caller that set the + // field meant it, and the combination cannot be put on the wire. + if self.cookie_hash.is_some() && self.udp_ver != UdpVersion::V3 { + return Err(ironrdp_core::invalid_field_err!( + Self::NAME, + "cookieHash", + "is only carried when uUdpVer is version 3" + )); + } + + ironrdp_core::ensure_size!(in: dst, size: self.encoded_size()); + + dst.write_u16_be(self.syn_ex_flags.bits()); + dst.write_u16_be(self.udp_ver.as_u16()); + + if let Some(hash) = &self.cookie_hash { + dst.write_slice(hash); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.encoded_size() + } +} + +impl Decode<'_> for SynDataExPayload { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ironrdp_core::ensure_fixed_part_size!(in: src); + + let syn_ex_flags_raw = src.read_u16_be(); + let syn_ex_flags = SynExFlags::from_bits_truncate(syn_ex_flags_raw); + + let udp_ver_raw = src.read_u16_be(); + let udp_ver = UdpVersion::from_u16(udp_ver_raw).ok_or_else(|| { + ironrdp_core::invalid_field_err!("RDPUDP_SYNDATAEX_PAYLOAD", "uUdpVer", "unknown protocol version") + })?; + + // cookieHash is present only for v3 and when there are enough remaining bytes. + // Per spec: MUST be present in client→server SYN with v3, MUST NOT be present otherwise. + // We detect its presence by checking for remaining bytes because the caller may not + // know the role (client vs server) at the PDU layer. + let cookie_hash = if udp_ver == UdpVersion::V3 && src.len() >= Self::COOKIE_HASH_SIZE { + Some(src.read_array::<32>()) + } else { + None + }; + + Ok(Self { + syn_ex_flags, + udp_ver, + cookie_hash, + }) + } +} diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml index 883ec3757..8fa56f4c2 100644 --- a/crates/ironrdp-testsuite-core/Cargo.toml +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -53,6 +53,7 @@ ironrdp-svc.path = "../ironrdp-svc" ironrdp-input.path = "../ironrdp-input" ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" ironrdp-rdpdr.path = "../ironrdp-rdpdr" +ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", features = ["std"] } ironrdp-rdpeusb.path = "../ironrdp-rdpeusb" ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", features = ["__test"] } ironrdp-server.path = "../ironrdp-server" diff --git a/crates/ironrdp-testsuite-core/tests/main.rs b/crates/ironrdp-testsuite-core/tests/main.rs index 35cac0e7b..0322ebec3 100644 --- a/crates/ironrdp-testsuite-core/tests/main.rs +++ b/crates/ironrdp-testsuite-core/tests/main.rs @@ -28,6 +28,7 @@ mod pdu; mod propertyset; mod rdcleanpath; mod rdpdr; +mod rdpeudp; mod rdpeusb; mod rdpsnd; mod server; diff --git a/crates/ironrdp-testsuite-core/tests/rdpeudp/mod.rs b/crates/ironrdp-testsuite-core/tests/rdpeudp/mod.rs new file mode 100644 index 000000000..8dd1cd3bb --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdpeudp/mod.rs @@ -0,0 +1,3 @@ +mod pdu_v1_datagram; +mod pdu_v1_header; +mod pdu_v1_syn; diff --git a/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_datagram.rs b/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_datagram.rs new file mode 100644 index 000000000..b3f2baaa1 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_datagram.rs @@ -0,0 +1,444 @@ +use ironrdp_core::{DecodeResult, Encode as _, decode, encode_vec}; +use ironrdp_rdpeudp::pdu::*; +// ════════════════════════════════════════════════════════════════ +// V1Datagram tests +// ════════════════════════════════════════════════════════════════ + +/// Client SYN: header + SYNDATA + SYNDATAEX. +#[test] +fn v1_syn_datagram_roundtrip() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0xFFFF_FFFF, + receive_window_size: 64, + flags: V1Flags::SYN | V1Flags::SYNEX, + }, + ack_vector: None, + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 0x1234_5678, + upstream_mtu: 1232, + downstream_mtu: 1232, + }), + correlation_id: None, + syn_data_ex: Some(SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V2, + cookie_hash: None, + }), + }; + + // header(8) + syndata(8) + syndataex(4) = 20 bytes + assert_eq!(datagram.size(), 20); + + let encoded = encode_vec(&datagram).expect("encode"); + assert_eq!(encoded.len(), 20); + + let decoded: V1Datagram = decode(&encoded).expect("decode"); + assert_eq!(decoded, datagram); +} + +/// Server SYN+ACK: header + SYNDATA + SYNDATAEX. +/// +/// No ACK vector, despite the ACK flag: see [MS-RDPEUDP] 3.1.5.1.3. +#[test] +fn v1_syn_ack_datagram_roundtrip() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 100, + receive_window_size: 64, + flags: V1Flags::SYN | V1Flags::ACK | V1Flags::SYNEX, + }, + ack_vector: None, + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 0xAABB_CCDD, + upstream_mtu: 1200, + downstream_mtu: 1200, + }), + correlation_id: None, + syn_data_ex: Some(SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V2, + cookie_hash: None, + }), + }; + + // header(8) + syndata(8) + syndataex(4) = 20. + assert_eq!(datagram.size(), 20); + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + assert_eq!(decoded, datagram); + assert!(decoded.header.flags.contains(V1Flags::ACK)); +} + +/// The SYN+ACK capture from [MS-RDPEUDP] 4.1.2, decoded whole. +/// +/// The ACK flag is set and the SYNDATA payload starts immediately after the +/// 8-byte header, so a decoder that reads an ACK vector here consumes the +/// first half of SYNDATA and desynchronises for the rest of the datagram. +#[test] +fn v1_decode_the_spec_syn_ack_capture() { + // Trailing zeroes are the start of the pad to uUpStreamMtu. + const CAPTURE: [u8; 19] = [ + 0x00, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x42, 0x04, 0xD0, 0x04, 0xD0, 0x00, 0x00, + 0x00, + ]; + + let datagram: V1Datagram = decode(&CAPTURE).expect("decode"); + + assert_eq!(datagram.header.sn_source_ack, 0x42); + assert_eq!(datagram.header.receive_window_size, 1024); + assert_eq!(datagram.header.flags, V1Flags::SYN | V1Flags::ACK); + assert!(datagram.ack_vector.is_none()); + + let syn_data = datagram.syn_data.expect("SYNDATA"); + assert_eq!(syn_data.initial_sequence_number, 0x42); + assert_eq!(syn_data.upstream_mtu, 1232); + assert_eq!(syn_data.downstream_mtu, 1232); + + assert!(datagram.syn_data_ex.is_none()); +} + +/// The ACK capture from [MS-RDPEUDP] 4.2.3, whose vector we do decode. +/// +/// The counterpart to the SYN+ACK above: without SYN, the ACK flag means what +/// 2.2.2.1 says it means. The DATA payload is left off, since v1 data transfer +/// is out of scope for this crate. +#[test] +fn v1_decode_the_spec_ack_capture() { + const CAPTURE: [u8; 16] = [ + // FEC header: snSourceAck, uReceiveWindowSize 1024, uFlags 0x0104. + 0xD6, 0xCF, 0x0A, 0xB8, 0x04, 0x00, 0x01, 0x04, + // ACK vector: one element, 4 datagrams received, then a pad byte. + 0x00, 0x01, 0x04, 0x00, // + // AckOfAcks. + 0xD6, 0xCF, 0x0A, 0xB8, + ]; + + let datagram: V1Datagram = decode(&CAPTURE).expect("decode"); + + assert_eq!(datagram.header.sn_source_ack, 0xD6CF_0AB8); + assert_eq!(datagram.header.receive_window_size, 1024); + + let ack_vector = datagram.ack_vector.as_ref().expect("ACK vector"); + assert_eq!( + ack_vector.elements, + vec![V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 4, + }] + ); + + assert_eq!( + datagram.ack_of_acks.as_ref().expect("AckOfAcks").reset_seq_num, + 0xD6CF_0AB8 + ); + + assert_eq!(encode_vec(&datagram).expect("encode"), CAPTURE); +} + +/// A SYN datagram carrying an ACK vector is rejected rather than written. +#[test] +fn v1_encode_rejects_ack_vector_on_a_syn() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 100, + receive_window_size: 64, + flags: V1Flags::SYN | V1Flags::ACK, + }, + ack_vector: Some(V1AckVectorHeader { + elements: vec![V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 1, + }], + }), + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 1, + upstream_mtu: 1232, + downstream_mtu: 1232, + }), + correlation_id: None, + syn_data_ex: None, + }; + + encode_vec(&datagram).expect_err("a SYN datagram cannot carry an ACK vector"); +} + +/// Client final ACK: header + ack_vector + ack_of_acks. +#[test] +fn v1_ack_datagram_roundtrip() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 200, + receive_window_size: 64, + flags: V1Flags::ACK | V1Flags::ACK_OF_ACKS, + }, + ack_vector: Some(V1AckVectorHeader { + elements: vec![ + V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 5, + }, + V1AckVectorElement { + state: VectorElementState::DatagramNotYetReceived, + length: 2, + }, + ], + }), + ack_of_acks: Some(V1AckOfAcksHeader { reset_seq_num: 150 }), + syn_data: None, + correlation_id: None, + syn_data_ex: None, + }; + + // header(8) + ack_vector(2+2=4) + ack_of_acks(4) = 16 + assert_eq!(datagram.size(), 16); + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + assert_eq!(decoded, datagram); +} + +/// SYN with correlation ID. +#[test] +fn v1_syn_with_correlation_id_roundtrip() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0xFFFF_FFFF, + receive_window_size: 64, + flags: V1Flags::SYN | V1Flags::SYNEX | V1Flags::CORRELATION_ID, + }, + ack_vector: None, + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 42, + upstream_mtu: 1232, + downstream_mtu: 1132, + }), + correlation_id: Some(CorrelationIdPayload { + correlation_id: [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + ], + }), + syn_data_ex: Some(SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V2, + cookie_hash: None, + }), + }; + + // header(8) + syndata(8) + correlation(16 id + 16 reserved = 32) + syndataex(4) = 52. + // [MS-RDPEUDP] 2.2.2.8 makes RDPUDP_CORRELATION_ID_PAYLOAD 32 bytes. + assert_eq!(datagram.size(), 52); + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + assert_eq!(decoded, datagram); +} + +/// V3 SYN with 32-byte cookie hash. +#[test] +fn v1_syn_v3_with_cookie_roundtrip() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0xFFFF_FFFF, + receive_window_size: 64, + flags: V1Flags::SYN | V1Flags::SYNEX, + }, + ack_vector: None, + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 0xDEAD_BEEF, + upstream_mtu: 1232, + downstream_mtu: 1232, + }), + correlation_id: None, + syn_data_ex: Some(SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V3, + cookie_hash: Some([0xAA; 32]), + }), + }; + + // header(8) + syndata(8) + syndataex(4+32=36) = 52 + assert_eq!(datagram.size(), 52); + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + assert_eq!(decoded, datagram); +} + +/// Verify flags are auto-computed from populated fields. +#[test] +fn v1_flags_auto_computed_on_encode() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0xFFFF_FFFF, + receive_window_size: 64, + // Caller set neither ACK nor ACK_OF_ACKS, but the payloads + // those flags gate are populated. + flags: V1Flags::empty(), + }, + ack_vector: Some(V1AckVectorHeader { elements: vec![] }), + ack_of_acks: Some(V1AckOfAcksHeader { reset_seq_num: 7 }), + syn_data: None, + correlation_id: None, + // syn_data_ex is None, so SYNEX should NOT be in flags + syn_data_ex: None, + }; + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + + // ACK should be auto-added (ack_vector is Some) + assert!(decoded.header.flags.contains(V1Flags::ACK)); + // ACK_OF_ACKS should be auto-added (ack_of_acks is Some) + assert!(decoded.header.flags.contains(V1Flags::ACK_OF_ACKS)); + // SYNEX should NOT be set (syn_data_ex is None) + assert!(!decoded.header.flags.contains(V1Flags::SYNEX)); +} + +/// The ACK flag survives on a SYN+ACK, where no payload implies it. +#[test] +fn v1_ack_flag_preserved_on_a_syn() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 100, + receive_window_size: 64, + flags: V1Flags::ACK, + }, + ack_vector: None, + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 1, + upstream_mtu: 1232, + downstream_mtu: 1232, + }), + correlation_id: None, + syn_data_ex: None, + }; + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + + assert_eq!(decoded.header.flags, V1Flags::SYN | V1Flags::ACK); + assert!(decoded.ack_vector.is_none()); +} + +/// A plain SYN does not acquire the ACK flag. +#[test] +fn v1_ack_flag_absent_on_a_bare_syn() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0xFFFF_FFFF, + receive_window_size: 64, + flags: V1Flags::empty(), + }, + ack_vector: None, + ack_of_acks: None, + syn_data: Some(SynDataPayload { + initial_sequence_number: 1, + upstream_mtu: 1232, + downstream_mtu: 1232, + }), + correlation_id: None, + syn_data_ex: None, + }; + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + + assert_eq!(decoded.header.flags, V1Flags::SYN); +} + +/// Standalone flags (CN, CWR, ACKDELAYED) are preserved on encode. +#[test] +fn v1_standalone_flags_preserved() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 100, + receive_window_size: 64, + flags: V1Flags::ACK | V1Flags::CN | V1Flags::ACKDELAYED, + }, + ack_vector: Some(V1AckVectorHeader { + elements: vec![V1AckVectorElement { + state: VectorElementState::DatagramReceived, + length: 10, + }], + }), + ack_of_acks: None, + syn_data: None, + correlation_id: None, + syn_data_ex: None, + }; + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + + assert!(decoded.header.flags.contains(V1Flags::CN)); + assert!(decoded.header.flags.contains(V1Flags::ACKDELAYED)); + assert!(decoded.header.flags.contains(V1Flags::ACK)); +} + +/// Reject datagrams with DATA flag (not supported in handshake). +#[test] +fn v1_decode_rejects_data_flag() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0, + receive_window_size: 64, + flags: V1Flags::empty(), + }, + ack_vector: None, + ack_of_acks: None, + syn_data: None, + correlation_id: None, + syn_data_ex: None, + }; + let mut encoded = encode_vec(&datagram).expect("encode"); + + // Manually set DATA flag in the wire bytes + // Flags are at offset 6..8 in FecHeader (after snSourceAck(4) + windowSize(2)) + let flags_raw = u16::from_le_bytes([encoded[6], encoded[7]]); + let modified = flags_raw | V1Flags::DATA.bits(); + let [low, high] = modified.to_le_bytes(); + encoded[6] = low; + encoded[7] = high; + + let result: DecodeResult = decode(&encoded); + assert!(result.is_err()); +} + +/// Minimal datagram: just a header with no payloads. +#[test] +fn v1_empty_datagram_roundtrip() { + let datagram = V1Datagram { + header: FecHeader { + sn_source_ack: 0, + receive_window_size: 32, + flags: V1Flags::empty(), + }, + ack_vector: None, + ack_of_acks: None, + syn_data: None, + correlation_id: None, + syn_data_ex: None, + }; + + assert_eq!(datagram.size(), 8); // header only + + let encoded = encode_vec(&datagram).expect("encode"); + let decoded: V1Datagram = decode(&encoded).expect("decode"); + assert_eq!(decoded, datagram); +} + +/// Insufficient bytes for header. +#[test] +fn v1_insufficient_bytes() { + let bytes = [0x00, 0x00, 0x00]; // need 8 for header + let result: DecodeResult = decode(&bytes); + assert!(result.is_err()); +} diff --git a/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_header.rs b/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_header.rs new file mode 100644 index 000000000..ea339e491 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_header.rs @@ -0,0 +1,88 @@ +use ironrdp_core::{DecodeResult, Encode as _, decode, encode_vec}; +use ironrdp_rdpeudp::pdu::*; +/// SYN datagram header per [MS-RDPEUDP] 3.1.5.1.1. +/// +/// All multi-byte fields are in network byte order, which 2.2 requires for +/// every message this protocol puts on the wire. That is the opposite of +/// MS-RDPEUDP2, whose 2.2 mandates little-endian. +const SYN_HEADER_BYTES: [u8; 8] = [ + 0xFF, 0xFF, 0xFF, 0xFF, // snSourceAck = 0xFFFFFFFF + 0x00, 0x40, // uReceiveWindowSize = 64 + 0x10, 0x01, // uFlags = SYN(0x0001) | SYNEX(0x1000) = 0x1001 +]; + +fn syn_header() -> FecHeader { + FecHeader { + sn_source_ack: 0xFFFF_FFFF, + receive_window_size: 64, + flags: V1Flags::SYN | V1Flags::SYNEX, + } +} + +#[test] +fn encode_syn_header() { + let encoded = encode_vec(&syn_header()).expect("encode should succeed"); + assert_eq!(encoded.as_slice(), &SYN_HEADER_BYTES); +} + +#[test] +fn decode_syn_header() { + let decoded: FecHeader = decode(&SYN_HEADER_BYTES).expect("decode should succeed"); + assert_eq!(decoded, syn_header()); +} + +#[test] +fn roundtrip() { + let original = FecHeader { + sn_source_ack: 0x0000_1234, + receive_window_size: 128, + flags: V1Flags::ACK | V1Flags::CN | V1Flags::ACK_OF_ACKS, + }; + let encoded = encode_vec(&original).expect("encode"); + let decoded: FecHeader = decode(&encoded).expect("decode"); + assert_eq!(original, decoded); +} + +#[test] +fn size_matches_encoding() { + let header = syn_header(); + let encoded = encode_vec(&header).expect("encode"); + assert_eq!(header.size(), encoded.len()); +} + +#[test] +fn decode_insufficient_bytes() { + let short = [0xFF, 0xFF, 0xFF]; // only 3 bytes, need 8 + let result: DecodeResult = decode(&short); + assert!(result.is_err()); +} + +/// The SYN capture from [MS-RDPEUDP] 4.1.1, which is the authority for the +/// byte order. +#[test] +fn decode_the_spec_syn_header_capture() { + // ff ff ff ff 04 00 0A 01, documented as uReceiveWindowSize 0x0400 = 1024 + // and uFlags 0x0A01 = CORRELATION_ID | SYNLOSSY | SYN. + const CAPTURE: [u8; 8] = [0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0x00, 0x0A, 0x01]; + + let header: FecHeader = decode(&CAPTURE).expect("decode"); + assert_eq!(header.sn_source_ack, 0xFFFF_FFFF); + assert_eq!(header.receive_window_size, 1024); + assert_eq!(header.flags, V1Flags::SYN | V1Flags::SYNLOSSY | V1Flags::CORRELATION_ID); + + assert_eq!(encode_vec(&header).expect("encode"), CAPTURE); +} + +/// The SYN and ACK capture from [MS-RDPEUDP] 4.1.2. +#[test] +fn decode_the_spec_syn_ack_header_capture() { + // 00 00 00 42 04 00 00 05, documented as uFlags 0x0005 = SYN | ACK. + const CAPTURE: [u8; 8] = [0x00, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, 0x05]; + + let header: FecHeader = decode(&CAPTURE).expect("decode"); + assert_eq!(header.sn_source_ack, 0x42); + assert_eq!(header.receive_window_size, 1024); + assert_eq!(header.flags, V1Flags::SYN | V1Flags::ACK); + + assert_eq!(encode_vec(&header).expect("encode"), CAPTURE); +} diff --git a/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_syn.rs b/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_syn.rs new file mode 100644 index 000000000..548534be0 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/rdpeudp/pdu_v1_syn.rs @@ -0,0 +1,270 @@ +use ironrdp_core::{DecodeResult, Encode as _, decode, encode_vec}; +use ironrdp_rdpeudp::pdu::*; +// -- SynDataPayload tests -- + +// Network byte order, per [MS-RDPEUDP] 2.2. +const SYNDATA_BYTES: [u8; 8] = [ + 0x12, 0x34, 0x56, 0x78, // snInitialSequenceNumber = 0x12345678 + 0x04, 0xD0, // uUpStreamMtu = 1232 + 0x04, 0x6C, // uDownStreamMtu = 1132 +]; + +fn syndata() -> SynDataPayload { + SynDataPayload { + initial_sequence_number: 0x1234_5678, + upstream_mtu: 1232, + downstream_mtu: 1132, + } +} + +#[test] +fn encode_syndata() { + let encoded = encode_vec(&syndata()).expect("encode"); + assert_eq!(encoded.as_slice(), &SYNDATA_BYTES); +} + +#[test] +fn decode_syndata() { + let decoded: SynDataPayload = decode(&SYNDATA_BYTES).expect("decode"); + assert_eq!(decoded, syndata()); +} + +#[test] +fn syndata_roundtrip() { + let original = syndata(); + let encoded = encode_vec(&original).expect("encode"); + let decoded: SynDataPayload = decode(&encoded).expect("decode"); + assert_eq!(original, decoded); +} + +#[test] +fn syndata_size() { + assert_eq!(syndata().size(), 8); +} + +#[test] +fn syndata_mtu_below_minimum() { + let mut bad = SYNDATA_BYTES; + // Set uUpStreamMtu to 1000 (below 1132) + bad[4] = 0xE8; + bad[5] = 0x03; + let result: DecodeResult = decode(&bad); + assert!(result.is_err()); +} + +#[test] +fn syndata_mtu_above_maximum() { + let mut bad = SYNDATA_BYTES; + // Set uDownStreamMtu to 2000 (above 1232) + bad[6] = 0xD0; + bad[7] = 0x07; + let result: DecodeResult = decode(&bad); + assert!(result.is_err()); +} + +#[test] +fn syndata_mtu_boundary_values() { + // Both at minimum + let min_mtu = SynDataPayload { + initial_sequence_number: 1, + upstream_mtu: MTU_MIN, + downstream_mtu: MTU_MIN, + }; + let encoded = encode_vec(&min_mtu).expect("encode"); + let decoded: SynDataPayload = decode(&encoded).expect("decode"); + assert_eq!(min_mtu, decoded); + + // Both at maximum + let max_mtu = SynDataPayload { + initial_sequence_number: 1, + upstream_mtu: MTU_MAX, + downstream_mtu: MTU_MAX, + }; + let encoded = encode_vec(&max_mtu).expect("encode"); + let decoded: SynDataPayload = decode(&encoded).expect("decode"); + assert_eq!(max_mtu, decoded); +} + +// -- SynDataExPayload tests -- + +#[test] +fn encode_syndataex_v2_no_cookie() { + let payload = SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V2, + cookie_hash: None, + }; + let encoded = encode_vec(&payload).expect("encode"); + assert_eq!( + encoded.as_slice(), + &[ + 0x00, 0x01, // uSynExFlags = VERSION_INFO_VALID + 0x00, 0x02, // uUdpVer = V2 + ] + ); + assert_eq!(payload.size(), 4); +} + +#[test] +fn decode_syndataex_v2_no_cookie() { + let bytes = [0x00, 0x01, 0x00, 0x02]; + let decoded: SynDataExPayload = decode(&bytes).expect("decode"); + assert_eq!(decoded.udp_ver, UdpVersion::V2); + assert!(decoded.cookie_hash.is_none()); +} + +#[test] +fn encode_syndataex_v3_with_cookie() { + let mut cookie = [0u8; 32]; + for (i, byte) in cookie.iter_mut().enumerate() { + *byte = u8::try_from(i % 256).expect("modulo 256 fits in u8"); + } + + let payload = SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V3, + cookie_hash: Some(cookie), + }; + let encoded = encode_vec(&payload).expect("encode"); + assert_eq!(payload.size(), 36); // 4 + 32 + assert_eq!(encoded.len(), 36); + + // Verify cookie hash bytes + assert_eq!(&encoded[4..36], &cookie); +} + +#[test] +fn decode_syndataex_v3_with_cookie() { + let mut bytes = vec![ + 0x01, 0x00, // VERSION_INFO_VALID + 0x01, 0x01, // V3 = 0x0101 + ]; + let cookie: Vec = (0..32).collect(); + bytes.extend_from_slice(&cookie); + + let decoded: SynDataExPayload = decode(&bytes).expect("decode"); + assert_eq!(decoded.udp_ver, UdpVersion::V3); + let hash = decoded.cookie_hash.expect("cookie hash should be present"); + assert_eq!(hash.as_slice(), cookie.as_slice()); +} + +#[test] +fn syndataex_roundtrip_v2() { + let original = SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V2, + cookie_hash: None, + }; + let encoded = encode_vec(&original).expect("encode"); + let decoded: SynDataExPayload = decode(&encoded).expect("decode"); + assert_eq!(original, decoded); +} + +#[test] +fn syndataex_roundtrip_v3_with_cookie() { + let original = SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V3, + cookie_hash: Some([0xAB; 32]), + }; + let encoded = encode_vec(&original).expect("encode"); + let decoded: SynDataExPayload = decode(&encoded).expect("decode"); + assert_eq!(original, decoded); +} + +#[test] +fn syndataex_unknown_version() { + let bytes = [ + 0x01, 0x00, // VERSION_INFO_VALID + 0xFF, 0xFF, // unknown version + ]; + let result: DecodeResult = decode(&bytes); + assert!(result.is_err()); +} + +#[test] +fn syndataex_insufficient_bytes() { + let bytes = [0x01, 0x00]; // only 2 bytes, need 4 + let result: DecodeResult = decode(&bytes); + assert!(result.is_err()); +} + +// -- UdpVersion tests -- + +#[test] +fn version_wire_values() { + assert_eq!(UdpVersion::V1.as_u16(), 0x0001); + assert_eq!(UdpVersion::V2.as_u16(), 0x0002); + assert_eq!(UdpVersion::V3.as_u16(), 0x0101); +} + +/// Only version 3 selects the MS-RDPEUDP2 data transfer. +/// +/// The name of version 2 suggests otherwise. [MS-RDPEUDP] 1.3.2.2 is explicit: +/// the MS-RDPEUDP data transfer messages "MUST be used only when the version +/// negotiated in the UDP connection initialization phase is version 1 or +/// version 2", and the 2.2.2.9 table mentions [MS-RDPEUDP2] on the 0x0101 row +/// alone. +#[test] +fn only_version_3_selects_the_v2_wire_format() { + assert!(!UdpVersion::V1.uses_v2_wire_format()); + assert!(!UdpVersion::V2.uses_v2_wire_format()); + assert!(UdpVersion::V3.uses_v2_wire_format()); +} + +#[test] +fn version_timer_minimums() { + assert_eq!(UdpVersion::V1.min_retransmit_ms(), 500); + assert_eq!(UdpVersion::V2.min_retransmit_ms(), 300); + assert_eq!(UdpVersion::V3.min_retransmit_ms(), 300); + + assert_eq!(UdpVersion::V1.min_ack_delay_ms(), 200); + assert_eq!(UdpVersion::V2.min_ack_delay_ms(), 50); + assert_eq!(UdpVersion::V3.min_ack_delay_ms(), 50); +} + +/// The cookie hash rides only on version 3, so any other version carrying one +/// must be rejected rather than encoded and silently dropped on the way back. +/// +/// The decoder reads the hash only for version 3; the encoder used to write it +/// for any version, so 32 bytes went out that no peer would read back. +/// Found by the `rdpeudp_pdu_round_trip` fuzz oracle. +#[test] +fn syn_data_ex_rejects_a_cookie_hash_without_version_3() { + let payload = SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V2, + cookie_hash: Some([0u8; 32]), + }; + + encode_vec(&payload).expect_err("a cookie hash cannot ride on version 2"); +} + +/// Version 3 with a cookie hash round-trips. +#[test] +fn syn_data_ex_round_trips_a_version_3_cookie_hash() { + let payload = SynDataExPayload { + syn_ex_flags: SynExFlags::VERSION_INFO_VALID, + udp_ver: UdpVersion::V3, + cookie_hash: Some([0xAB; 32]), + }; + + let encoded = encode_vec(&payload).expect("encode"); + let decoded: SynDataExPayload = decode(&encoded).expect("decode"); + assert_eq!(decoded, payload); +} + +/// The SYNDATA payload from the [MS-RDPEUDP] 4.1.1 SYN capture. +#[test] +fn decode_the_spec_syndata_capture() { + // 00 00 00 42 04 D0 04 D0, documented as snInitialSequenceNumber 0x42 and + // both MTUs 0x04D0 = 1232. + const CAPTURE: [u8; 8] = [0x00, 0x00, 0x00, 0x42, 0x04, 0xD0, 0x04, 0xD0]; + + let payload: SynDataPayload = decode(&CAPTURE).expect("decode"); + assert_eq!(payload.initial_sequence_number, 0x42); + assert_eq!(payload.upstream_mtu, 1232); + assert_eq!(payload.downstream_mtu, 1232); + + assert_eq!(encode_vec(&payload).expect("encode"), CAPTURE); +}