Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion crates/ironrdp-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use ironrdp_propertyset::PropertySet;
use ironrdp_rail::pdu::ExecutePdu;
use url::Url;

/// RDP-UDP protocol version, as offered with [`ConfigBuilder::with_udp_offer_version`].
#[cfg(feature = "udp")]
pub use ironrdp_rdpeudp::pdu::UdpVersion;
#[cfg(feature = "vmconnect")]
pub use ironrdp_vmconnect::Mode as VmConnectMode;

Expand Down Expand Up @@ -93,6 +96,8 @@ pub struct Config {
pub(crate) transport: Transport,
#[cfg(feature = "udp")]
pub(crate) udp_transport_enabled: bool,
#[cfg(feature = "udp")]
pub(crate) udp_offer_version: UdpVersion,
pub(crate) certificate_validation: ironrdp_tls::CertificateValidation,
pub(crate) certificate_validation_callback: Option<ironrdp_tls::CertificateValidationCallback>,

Expand Down Expand Up @@ -164,6 +169,12 @@ impl Config {
self.udp_transport_enabled
}

/// Highest RDP-UDP protocol version offered in the SYN of the UDP handshake.
#[cfg(feature = "udp")]
pub fn udp_offer_version(&self) -> UdpVersion {
self.udp_offer_version
}

/// TLS peer-certificate validation policy.
pub fn certificate_validation(&self) -> ironrdp_tls::CertificateValidation {
self.certificate_validation
Expand Down Expand Up @@ -273,6 +284,8 @@ impl fmt::Debug for Config {
s.field("transport", &self.transport);
#[cfg(feature = "udp")]
s.field("udp_transport_enabled", &self.udp_transport_enabled);
#[cfg(feature = "udp")]
s.field("udp_offer_version", &self.udp_offer_version);
s.field("certificate_validation", &self.certificate_validation);
s.field(
"certificate_validation_callback",
Expand Down Expand Up @@ -784,6 +797,9 @@ pub struct ConfigBuilder {
transport: TransportKind,
#[cfg(feature = "udp")]
udp_transport_enabled: bool,
#[cfg(feature = "udp")]
udp_offer_version: Option<UdpVersion>,
graphics_pipeline: bool,
#[cfg(feature = "vmconnect")]
vm_id: Option<String>,
#[cfg(feature = "vmconnect")]
Expand Down Expand Up @@ -1281,6 +1297,30 @@ impl ConfigBuilder {
self
}

/// Selects the highest RDP-UDP protocol version the UDP handshake offers.
///
/// The default, [`UdpVersion::V3`], offers MS-RDPEUDP2 and lets the server settle on a
/// lower version. Offering [`UdpVersion::V2`] or [`UdpVersion::V1`] asks for MS-RDPEUDP
/// outright, which is what Windows servers that do not implement version 3 answer.
#[cfg(feature = "udp")]
#[must_use]
pub fn with_udp_offer_version(mut self, version: UdpVersion) -> Self {
self.udp_offer_version = Some(version);
self
}

/// Advertises the graphics pipeline ([MS-RDPEGFX]) to the server. Off by default.
///
/// Without it, the server keeps graphics on the legacy bitmap path, which travels on the
/// main connection and cannot move to a reliable UDP tunnel.
///
/// [MS-RDPEGFX]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegfx/
#[must_use]
pub fn with_graphics_pipeline(mut self, enabled: bool) -> Self {
self.graphics_pipeline = enabled;
self
}
Comment thread
AKolenda marked this conversation as resolved.

/// Connect to a Hyper-V VM console by VM GUID. Destination must use port
/// [`ironrdp_vmconnect::PORT`] (2179) unless the caller explicitly selects another port.
/// A destination with no explicit port defaults to 2179 instead of the ordinary RDP port.
Expand Down Expand Up @@ -1947,7 +1987,7 @@ impl ConfigBuilder {
request_data: None,
pointer_software_rendering: self.pointer_software_rendering.unwrap_or(false),
multitransport_flags: None,
support_dyn_vc_gfx_protocol: false,
support_dyn_vc_gfx_protocol: self.graphics_pipeline,
compression_type,
performance_flags: self.performance_flags.unwrap_or_default(),
timezone_info: TimezoneInfo::default(),
Expand Down Expand Up @@ -1982,6 +2022,8 @@ impl ConfigBuilder {
transport,
#[cfg(feature = "udp")]
udp_transport_enabled: self.udp_transport_enabled,
#[cfg(feature = "udp")]
udp_offer_version: self.udp_offer_version.unwrap_or(UdpVersion::V3),
certificate_validation,
certificate_validation_callback: self.certificate_validation_callback,
#[cfg(feature = "vmconnect")]
Expand Down
7 changes: 6 additions & 1 deletion crates/ironrdp-client/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,7 @@ struct UdpTunnel {
struct UdpBootstrapConfig {
peer: SocketAddr,
server_name: String,
offer_version: ironrdp_rdpeudp::pdu::UdpVersion,
tls: ironrdp_rdpeudp_tokio::UdpTlsConfig,
}

Expand All @@ -2034,7 +2035,10 @@ async fn bootstrap_udp_transport(
.connect(
config.peer,
config.server_name,
ironrdp_rdpeudp::ConnectionConfig::default(),
ironrdp_rdpeudp::ConnectionConfig {
offer_version: config.offer_version,
..ironrdp_rdpeudp::ConnectionConfig::default()
},
config.tls,
)
.await
Expand Down Expand Up @@ -2450,6 +2454,7 @@ where
let udp_config = UdpBootstrapConfig {
peer: udp_peer,
server_name: config.destination.name().to_owned(),
offer_version: config.udp_offer_version,
tls: ironrdp_rdpeudp_tokio::UdpTlsConfig {
certificate_validation: config.certificate_validation,
certificate_validation_callback: config.certificate_validation_callback.clone(),
Expand Down
76 changes: 75 additions & 1 deletion crates/ironrdp-testsuite-extra/tests/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use std::sync::Arc;
#[cfg(windows)]
use ironrdp_cfg::GatewayCredentialsSource;
use ironrdp_cfg::PropertySetExt as _;
use ironrdp_client::config::{AudioQualityMode, ClipboardType, ConfigBuilder, Destination, Transport, VmConnectMode};
use ironrdp_client::config::{
AudioQualityMode, ClipboardType, ConfigBuilder, Destination, Transport, UdpVersion, VmConnectMode,
};
#[cfg(windows)]
use ironrdp_client::config::{MissingField, TransportKind};
use ironrdp_pdu::gcc::{ClientMonitorData, Monitor, MonitorFlags};
Expand Down Expand Up @@ -681,6 +683,78 @@ fn reliable_udp_is_opt_in() {
assert!(config.udp_transport_enabled());
}

#[test]
fn udp_offer_defaults_to_version_3_and_can_be_lowered() {
let config = complete_builder().build().expect("default config");
assert_eq!(config.udp_offer_version(), UdpVersion::V3);

for version in [UdpVersion::V1, UdpVersion::V2, UdpVersion::V3] {
let config = complete_builder()
.with_udp_offer_version(version)
.build()
.expect("config with a UDP offer version");
assert_eq!(config.udp_offer_version(), version);
}
}

#[test]
fn graphics_pipeline_is_opt_in() {
let config = complete_builder().build().expect("default config");
assert!(!config.connector().support_dyn_vc_gfx_protocol);

let config = complete_builder()
.with_graphics_pipeline(true)
.build()
.expect("graphics pipeline config");
assert!(config.connector().support_dyn_vc_gfx_protocol);
}

#[test]
fn udp_and_graphics_pipeline_cli_flags_reach_the_config() {
let rdp = "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\n";

let config = parse_config_from_rdp(rdp, &[]);
assert!(!config.udp_transport_enabled());
assert_eq!(config.udp_offer_version(), UdpVersion::V3);
assert!(!config.connector().support_dyn_vc_gfx_protocol);

let config = parse_config_from_rdp(rdp, &["--udp", "--udp-offer", "2", "--egfx"]);
assert!(config.udp_transport_enabled());
assert_eq!(config.udp_offer_version(), UdpVersion::V2);
assert!(config.connector().support_dyn_vc_gfx_protocol);

let config = parse_config_from_rdp(rdp, &["--udp", "--udp-offer", "1"]);
assert_eq!(config.udp_offer_version(), UdpVersion::V1);
}

#[test]
fn udp_offer_without_udp_is_rejected() {
let rdp = "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\n";

let error = parse_config_from_rdp_result(rdp, &["--udp-offer", "2"])
.expect_err("--udp-offer is only used with --udp, so it must not be accepted alone");
assert!(error.to_string().contains("required arguments"), "{error:?}");
assert!(error.to_string().contains("--udp"), "{error:?}");

// Turning UDP off explicitly leaves nothing to be surprised by.
let config = parse_config_from_rdp(rdp, &["--udp=false", "--udp-offer", "2"]);
assert!(!config.udp_transport_enabled());
}

#[test]
fn udp_offer_requires_udp_in_rpc_mode() {
let error = ironrdp_viewer::cli::ViewerConfig::parse_from(["ironrdp-viewer", "--rpc", "--udp-offer", "2"])
.err()
.expect("clap must validate UDP arguments before entering RPC mode");
assert!(error.to_string().contains("required arguments"), "{error:?}");
assert!(error.to_string().contains("--udp"), "{error:?}");

assert!(
ironrdp_viewer::cli::ViewerConfig::parse_from(["ironrdp-viewer", "--rpc", "--udp=false", "--udp-offer", "2",])
.is_ok()
);
}

#[test]
fn remote_application_mode_requires_remote_programs_support() {
let error = complete_builder()
Expand Down
33 changes: 31 additions & 2 deletions crates/ironrdp-viewer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ struct Args {
#[clap(long, value_enum, default_value_t = KeyboardType::IbmEnhanced)]
keyboard_type: KeyboardType,

/// Enable reliable RDP-UDP for direct connections, falling back to TCP
#[clap(long, env = "IRONRDP_UDP", value_parser = clap::builder::BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
udp: Option<bool>,

/// Highest RDP-UDP protocol version to offer (1, 2 or 3). Requires --udp
#[clap(long, env = "IRONRDP_UDP_OFFER", requires = "udp", value_parser = clap::value_parser!(u16).range(1..=3))]
udp_offer: Option<u16>,

/// Advertise the graphics pipeline (MS-RDPEGFX)
#[clap(long, env = "IRONRDP_EGFX", value_parser = clap::builder::BoolishValueParser::new(), num_args = 0..=1, default_missing_value = "true")]
egfx: Option<bool>,

/// The keyboard subtype (an original equipment manufacturer-dependent value)
#[clap(long, default_value_t = 0)]
keyboard_subtype: u32,
Expand Down Expand Up @@ -282,16 +294,19 @@ pub struct ViewerConfig {

impl ViewerConfig {
pub fn parse_args() -> anyhow::Result<Self> {
Self::parse_from(std::env::args_os())
Self::from_args(Args::parse())
}

/// Parses arguments without exiting the process on a command-line error.
pub fn parse_from<I, T>(args: I) -> anyhow::Result<Self>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let args = Args::parse_from(args);
Self::from_args(Args::try_parse_from(args)?)
}

fn from_args(args: Args) -> anyhow::Result<Self> {
let mut properties = ironrdp_propertyset::PropertySet::new();

if let Some(rdp_file) = &args.rdp_file {
Expand Down Expand Up @@ -411,6 +426,20 @@ fn apply_cli_to_builder(
if let Some(scale) = args.scale_desktop {
builder = builder.with_desktop_scale_factor(scale);
}
if let Some(enabled) = args.udp {
builder = builder.with_udp_transport(enabled);
}
if let Some(version) = args.udp_offer {
use ironrdp::client::config::UdpVersion;
builder = builder.with_udp_offer_version(match version {
1 => UdpVersion::V1,
2 => UdpVersion::V2,
_ => UdpVersion::V3,
});
}
if let Some(enabled) = args.egfx {
builder = builder.with_graphics_pipeline(enabled);
}
if let Some(width) = args.desktop_width {
builder = builder.with_desktop_width(width);
}
Expand Down
Loading