From 1b16cb253727bc944c001239cf62fe82e8b03451 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 2 Jun 2025 15:16:14 -0400 Subject: [PATCH 1/3] Working --- Cargo.lock | 15 ++- Cargo.toml | 1 + crates/ironrdp-async/src/connector.rs | 2 +- crates/ironrdp-client/Cargo.toml | 1 + crates/ironrdp-client/src/config.rs | 6 + crates/ironrdp-client/src/rdp.rs | 80 +++++++++---- crates/ironrdp-connector/src/credssp.rs | 3 +- crates/ironrdp-vmconnect/Cargo.toml | 29 +++++ crates/ironrdp-vmconnect/src/lib.rs | 151 ++++++++++++++++++++++++ 9 files changed, 260 insertions(+), 28 deletions(-) create mode 100644 crates/ironrdp-vmconnect/Cargo.toml create mode 100644 crates/ironrdp-vmconnect/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cb1bc6d35..9d64361f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2416,6 +2416,7 @@ dependencies = [ "ironrdp-rdpsnd-native", "ironrdp-tls", "ironrdp-tokio", + "ironrdp-vmconnect", "proc-exit", "raw-window-handle", "semver", @@ -2780,6 +2781,18 @@ dependencies = [ "url", ] +[[package]] +name = "ironrdp-vmconnect" +version = "0.1.0" +dependencies = [ + "bytes", + "ironrdp-async", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "tracing", +] + [[package]] name = "ironrdp-web" version = "0.0.0" @@ -4951,8 +4964,6 @@ dependencies = [ [[package]] name = "sspi" version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27fb016d04750b9ce0b4576bfcdb978cf26a234536f2f6a7697b703e0f5bc048" dependencies = [ "async-dnssd", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index 004d84ddb..6e318b8ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,3 +149,4 @@ opt-level = 3 # FIXME: We need to catch up with Diplomat upstream again, but this is a significant amount of work. # In the meantime, we use this forked version which fixes an undefined behavior in the code expanded by the bridge macro. diplomat = { git = "https://github.com/CBenoit/diplomat", rev = "6dc806e80162b6b39509a04a2835744236cd2396" } +sspi = { path = "../sspi-rs" } \ No newline at end of file diff --git a/crates/ironrdp-async/src/connector.rs b/crates/ironrdp-async/src/connector.rs index 4d1b085dc..ba67d0859 100644 --- a/crates/ironrdp-async/src/connector.rs +++ b/crates/ironrdp-async/src/connector.rs @@ -106,7 +106,7 @@ async fn resolve_generator( } #[instrument(level = "trace", skip_all)] -async fn perform_credssp_step( +pub async fn perform_credssp_step( framed: &mut Framed, connector: &mut ClientConnector, buf: &mut WriteBuf, diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index 8c573121c..e034805a1 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -48,6 +48,7 @@ ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.3" } ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" } ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.4", features = ["reqwest"] } ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" +ironrdp-vmconnect.path = "../ironrdp-vmconnect" # Windowing and rendering winit = { version = "0.30", features = ["rwh_06"] } diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index 564a22fef..bc6bf4136 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -21,6 +21,7 @@ pub struct Config { pub connector: connector::Config, pub clipboard_type: ClipboardType, pub rdcleanpath: Option, + pub vmconnect: Option } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -238,6 +239,10 @@ struct Args { /// The bitmap codecs to use (remotefx:on, ...) #[clap(long, value_parser, num_args = 1.., value_delimiter = ',')] codecs: Vec, + + /// The ID for the HyperV VM server to connect to + #[clap(long, value_parser)] + vmconnect: Option, } impl Config { @@ -357,6 +362,7 @@ impl Config { connector, clipboard_type, rdcleanpath, + vmconnect: args.vmconnect, }) } } diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 3f950d1cf..adc396a0e 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -11,7 +11,7 @@ use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session}; use ironrdp_core::WriteBuf; use ironrdp_rdpsnd_native::cpal; use ironrdp_tokio::reqwest::ReqwestNetworkClient; -use ironrdp_tokio::{single_sequence_step_read, split_tokio_framed, FramedWrite}; +use ironrdp_tokio::{single_sequence_step_read, split_tokio_framed, Framed, FramedWrite, TokioStream}; use rdpdr::NoopRdpdrBackend; use smallvec::SmallVec; use tokio::io::{AsyncRead, AsyncWrite}; @@ -146,36 +146,67 @@ async fn connect( connector.attach_static_channel(cliprdr); } - let should_upgrade = ironrdp_tokio::connect_begin(&mut framed, &mut connector).await?; + let result = if let Some(vmconnect) = &config.vmconnect { + let should_upgrade = ironrdp_vmconnect::connect_begin(&mut framed, &mut connector, vmconnect).await?; + debug!("VMConnect upgrade"); + let (mut upgraded_framed, server_public_key) = upgrade(framed, config).await?; + let upgraded = ironrdp_vmconnect::mark_as_upgraded(should_upgrade, &mut connector); + + let connection_result = ironrdp_vmconnect::connect_finalize( + upgraded, + &mut upgraded_framed, + connector, + (&config.destination).into(), + server_public_key, + Some(&mut ReqwestNetworkClient::new()), + None, + ) + .await?; - debug!("TLS upgrade"); + (connection_result, upgraded_framed) + } else { + let should_upgrade = ironrdp_tokio::connect_begin(&mut framed, &mut connector).await?; - // Ensure there is no leftover - let (initial_stream, leftover_bytes) = framed.into_inner(); + debug!("TLS upgrade"); - let (upgraded_stream, server_public_key) = ironrdp_tls::upgrade(initial_stream, config.destination.name()) - .await - .map_err(|e| connector::custom_err!("TLS upgrade", e))?; + let (mut upgraded_framed, server_public_key) = upgrade(framed, config).await?; + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); - let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); + let connection_result = ironrdp_tokio::connect_finalize( + upgraded, + &mut upgraded_framed, + connector, + (&config.destination).into(), + server_public_key, + Some(&mut ReqwestNetworkClient::new()), + None, + ) + .await?; - let erased_stream = Box::new(upgraded_stream) as Box; - let mut upgraded_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased_stream, leftover_bytes); + debug!(?connection_result); + (connection_result, upgraded_framed) + }; - let connection_result = ironrdp_tokio::connect_finalize( - upgraded, - &mut upgraded_framed, - connector, - (&config.destination).into(), - server_public_key, - Some(&mut ReqwestNetworkClient::new()), - None, - ) - .await?; + return Ok(result); - debug!(?connection_result); + async fn upgrade( + framed: Framed>, + config: &Config, + ) -> ConnectorResult<( + Framed>>, + Vec, + )> { + let (initial_stream, leftover_bytes) = framed.into_inner(); - Ok((connection_result, upgraded_framed)) + let (upgraded_stream, server_public_key) = ironrdp_tls::upgrade(initial_stream, config.destination.name()) + .await + .map_err(|e| connector::custom_err!("TLS upgrade", e))?; + + let erased_stream = Box::new(upgraded_stream) as Box; + let upgraded_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased_stream, leftover_bytes); + + Ok((upgraded_framed, server_public_key)) + } } async fn connect_ws( @@ -255,7 +286,7 @@ async fn connect_ws( } async fn connect_rdcleanpath( - framed: &mut ironrdp_tokio::Framed, + framed: &mut Framed, connector: &mut connector::ClientConnector, destination: String, proxy_auth_token: String, @@ -306,6 +337,7 @@ where let rdcleanpath_req = ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb) .map_err(|e| connector::custom_err!("new RDCleanPath request", e))?; + debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); let rdcleanpath_req = rdcleanpath_req .to_der() diff --git a/crates/ironrdp-connector/src/credssp.rs b/crates/ironrdp-connector/src/credssp.rs index 0d2f8dec5..1c6be266f 100644 --- a/crates/ironrdp-connector/src/credssp.rs +++ b/crates/ironrdp-connector/src/credssp.rs @@ -213,7 +213,8 @@ impl CredsspSequence { ClientState::FinalMessage(ts_request) => ( ts_request, if self.selected_protocol.contains(nego::SecurityProtocol::HYBRID_EX) { - CredsspState::EarlyUserAuthResult + // Don't merge this, something needs to be done here for HyperV + CredsspState::Finished } else { CredsspState::Finished }, diff --git a/crates/ironrdp-vmconnect/Cargo.toml b/crates/ironrdp-vmconnect/Cargo.toml new file mode 100644 index 000000000..352825e94 --- /dev/null +++ b/crates/ironrdp-vmconnect/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ironrdp-vmconnect" +version = "0.1.0" +readme = "README.md" +description = "Provides `Future`s wrapping the IronRDP state machines conveniently" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-connector = { path = "../ironrdp-connector", version = "0.5" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = [ + "alloc", +] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.5" } # public +ironrdp-async = { path = "../ironrdp-async" } +tracing = { version = "0.1", features = ["log"] } +bytes = "1" # public + +[lints] +workspace = true diff --git a/crates/ironrdp-vmconnect/src/lib.rs b/crates/ironrdp-vmconnect/src/lib.rs new file mode 100644 index 000000000..af0995647 --- /dev/null +++ b/crates/ironrdp-vmconnect/src/lib.rs @@ -0,0 +1,151 @@ +#[macro_use] +extern crate tracing; + +use ironrdp_async::{perform_credssp_step, single_sequence_step, AsyncNetworkClient, Framed, FramedRead, FramedWrite}; +use ironrdp_connector::credssp::KerberosConfig; +use ironrdp_connector::{ + ClientConnector, ClientConnectorState, ConnectionResult, ConnectorResult, Sequence, ServerName, +}; +use ironrdp_core::WriteBuf; +use ironrdp_pdu::pcb::PcbVersion; +use tracing::instrument; + +#[non_exhaustive] +pub struct ShouldUpgrade; + +#[non_exhaustive] +pub struct Upgraded; + +pub async fn connect_begin( + framed: &mut Framed, + connector: &mut ClientConnector, + vm_id: &str, +) -> ConnectorResult +where + S: Sync + FramedRead + FramedWrite, +{ + info!("Pre-connection procedure"); + let mut buf = WriteBuf::new(); + debug_assert!(matches!( + connector.state, + ClientConnectorState::ConnectionInitiationSendRequest + )); + + let _ = connector.step(&[], &mut buf)?; + + let ClientConnectorState::ConnectionInitiationWaitConfirm { requested_protocol } = connector.state else { + return Err(ironrdp_connector::reason_err!( + "Invalid connector state", + "Expected ConnectionInitiationWaitConfirm", + )); + }; + + connector.state = ClientConnectorState::EnhancedSecurityUpgrade { + selected_protocol: requested_protocol, + }; + + let pdu = ironrdp_pdu::pcb::PreconnectionBlob { + id: 0, + version: PcbVersion::V2, + v2_payload: Some(format!("{vm_id};EnhancedMode=1")), + }; + + let to_write = ironrdp_core::encode_vec(&pdu) + .map_err(|e| ironrdp_connector::custom_err!("Failed to encode preconnection PDU", e))?; + + framed + .write_all(&to_write) + .await + .map_err(|e| ironrdp_connector::custom_err!("Failed to write preconnection PDU", e))?; + + Ok(ShouldUpgrade) +} + +#[instrument(skip_all)] +pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector) -> Upgraded { + trace!("Marked as upgraded"); + connector.mark_security_upgrade_as_done(); + Upgraded +} + +#[instrument(skip_all)] +pub async fn connect_begin_cleanpath( + framed: &mut Framed, + connector: &mut ClientConnector, + vm_id: &str, +) -> ConnectorResult +where + S: Sync + FramedRead + FramedWrite, +{ + info!("Pre-connection procedure"); + let mut buf = WriteBuf::new(); + debug_assert!(matches!( + connector.state, + ClientConnectorState::ConnectionInitiationSendRequest + )); + + let _ = connector.step(&[], &mut buf)?; + + let ClientConnectorState::ConnectionInitiationWaitConfirm { requested_protocol } = connector.state else { + return Err(ironrdp_connector::reason_err!( + "Invalid connector state", + "Expected ConnectionInitiationWaitConfirm", + )); + }; + + connector.state = ClientConnectorState::EnhancedSecurityUpgrade { + selected_protocol: requested_protocol, + }; + framed + .write_all(format!("{vm_id};EnhancedMode=1").as_bytes()) + .await + .map_err(|e| ironrdp_connector::custom_err!("Failed to write VM ID", e))?; + Ok(Upgraded) +} + +#[instrument(skip_all)] +pub async fn connect_finalize( + _: Upgraded, + framed: &mut Framed, + mut connector: ClientConnector, + server_name: ServerName, + server_public_key: Vec, + network_client: Option<&mut dyn AsyncNetworkClient>, + kerberos_config: Option, +) -> ConnectorResult +where + S: FramedRead + FramedWrite, +{ + let mut buf = WriteBuf::new(); + + if connector.should_perform_credssp() { + perform_credssp_step( + framed, + &mut connector, + &mut buf, + server_name, + server_public_key, + network_client, + kerberos_config, + ) + .await?; + } + + connector.state = ClientConnectorState::ConnectionInitiationSendRequest; + + let result = loop { + if let ClientConnectorState::EnhancedSecurityUpgrade { selected_protocol } = connector.state { + connector.state = ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol }; + } + + if let ClientConnectorState::Connected { result } = connector.state { + break result; + } + + single_sequence_step(framed, &mut connector, &mut buf).await?; + }; + + info!("Connected with success"); + + Ok(result) +} From 32ea7445445015ce2dbf8193b5f6c8a5f1b3e4bc Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 2 Jun 2025 17:35:45 -0400 Subject: [PATCH 2/3] at least it works --- crates/ironrdp-async/src/connector.rs | 3 + crates/ironrdp-blocking/src/connector.rs | 2 + crates/ironrdp-client/src/config.rs | 39 +++- crates/ironrdp-client/src/rdp.rs | 247 ++++++++++++++++------- crates/ironrdp-connector/src/credssp.rs | 10 +- crates/ironrdp-rdcleanpath/src/lib.rs | 24 +-- crates/ironrdp-vmconnect/src/lib.rs | 47 ++--- crates/ironrdp-web/src/session.rs | 2 +- 8 files changed, 247 insertions(+), 127 deletions(-) diff --git a/crates/ironrdp-async/src/connector.rs b/crates/ironrdp-async/src/connector.rs index ba67d0859..cae0f44d4 100644 --- a/crates/ironrdp-async/src/connector.rs +++ b/crates/ironrdp-async/src/connector.rs @@ -68,6 +68,7 @@ where server_public_key, network_client, kerberos_config, + false, // use_vmconnect ) .await?; } @@ -114,6 +115,7 @@ pub async fn perform_credssp_step( server_public_key: Vec, mut network_client: Option<&mut dyn AsyncNetworkClient>, kerberos_config: Option, + use_vmconnect: bool, ) -> ConnectorResult<()> where S: FramedRead + FramedWrite, @@ -132,6 +134,7 @@ where server_name, server_public_key, kerberos_config, + use_vmconnect, )?; loop { diff --git a/crates/ironrdp-blocking/src/connector.rs b/crates/ironrdp-blocking/src/connector.rs index 765560e6e..8ba1a031b 100644 --- a/crates/ironrdp-blocking/src/connector.rs +++ b/crates/ironrdp-blocking/src/connector.rs @@ -137,6 +137,8 @@ where server_name, server_public_key, kerberos_config, + // TODO: implement vmconnect support for ironrdp-blocking + false, )?; loop { diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index bc6bf4136..bc4b7df6e 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -21,7 +21,7 @@ pub struct Config { pub connector: connector::Config, pub clipboard_type: ClipboardType, pub rdcleanpath: Option, - pub vmconnect: Option + pub pcb: Option, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -243,6 +243,32 @@ struct Args { /// The ID for the HyperV VM server to connect to #[clap(long, value_parser)] vmconnect: Option, + + /// Preconnection Blob payload to use, cannot be used with `--vmconnect` + #[clap(long, value_parser)] + pcb: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreconnectionBlobPayload { + General(String), + VmConnect(String), +} + +impl PreconnectionBlobPayload { + pub fn general(&self) -> Option<&str> { + match self { + PreconnectionBlobPayload::General(pcb) => Some(pcb), + PreconnectionBlobPayload::VmConnect(_) => None, + } + } + + pub fn vmconnect(&self) -> Option<&str> { + match self { + PreconnectionBlobPayload::VmConnect(vm_id) => Some(vm_id), + PreconnectionBlobPayload::General(_) => None, + } + } } impl Config { @@ -356,13 +382,22 @@ impl Config { .zip(args.rdcleanpath_token) .map(|(url, auth_token)| RDCleanPathConfig { url, auth_token }); + let pcb = match (args.vmconnect, args.pcb) { + (Some(_), Some(_)) => { + anyhow::bail!("Cannot use both --vmconnect and --pcb options. Choose one of them."); + } + (Some(vm_id), None) => Some(PreconnectionBlobPayload::VmConnect(vm_id)), + (None, Some(pcb)) => Some(PreconnectionBlobPayload::General(pcb)), + (None, None) => None, + }; + Ok(Self { log_file: args.log_file, destination, connector, clipboard_type, rdcleanpath, - vmconnect: args.vmconnect, + pcb, }) } } diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index adc396a0e..6d4cab72c 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -18,8 +18,9 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tokio::sync::mpsc; use winit::event_loop::EventLoopProxy; +use x509_cert::der::asn1::OctetString; -use crate::config::{Config, RDCleanPathConfig}; +use crate::config::{Config, PreconnectionBlobPayload, RDCleanPathConfig}; #[derive(Debug)] pub enum RdpOutputEvent { @@ -146,7 +147,7 @@ async fn connect( connector.attach_static_channel(cliprdr); } - let result = if let Some(vmconnect) = &config.vmconnect { + let result = if let Some(PreconnectionBlobPayload::VmConnect(vmconnect)) = &config.pcb { let should_upgrade = ironrdp_vmconnect::connect_begin(&mut framed, &mut connector, vmconnect).await?; debug!("VMConnect upgrade"); let (mut upgraded_framed, server_public_key) = upgrade(framed, config).await?; @@ -263,20 +264,36 @@ async fn connect_ws( &mut connector, destination, rdcleanpath.auth_token.clone(), - None, + &config.pcb, ) .await?; - let connection_result = ironrdp_tokio::connect_finalize( - upgraded, - &mut framed, - connector, - (&config.destination).into(), - server_public_key, - Some(&mut ReqwestNetworkClient::new()), - None, - ) - .await?; + let connection_result = match upgraded { + Upgraded::VM(upgraded) => { + ironrdp_vmconnect::connect_finalize( + upgraded, + &mut framed, + connector, + (&config.destination).into(), + server_public_key, + Some(&mut ReqwestNetworkClient::new()), + None, + ) + .await? + } + Upgraded::Standard(upgraded) => { + ironrdp_tokio::connect_finalize( + upgraded, + &mut framed, + connector, + (&config.destination).into(), + server_public_key, + Some(&mut ReqwestNetworkClient::new()), + None, + ) + .await? + } + }; let (ws, leftover_bytes) = framed.into_inner(); let erased_stream = Box::new(ws) as Box; @@ -285,13 +302,18 @@ async fn connect_ws( Ok((connection_result, upgraded_framed)) } +pub(crate) enum Upgraded { + VM(ironrdp_vmconnect::Upgraded), + Standard(ironrdp_tokio::Upgraded), +} + async fn connect_rdcleanpath( framed: &mut Framed, connector: &mut connector::ClientConnector, destination: String, proxy_auth_token: String, - pcb: Option, -) -> ConnectorResult<(ironrdp_tokio::Upgraded, Vec)> + pcb: &Option, +) -> ConnectorResult<(Upgraded, Vec)> where S: ironrdp_tokio::FramedRead + FramedWrite, { @@ -318,80 +340,157 @@ where let mut buf = WriteBuf::new(); - info!("Begin connection procedure"); + info!(?pcb, "Begin connection procedure"); + let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else { + return Err(connector::general_err!("invalid connector state (send request)")); + }; + + debug_assert!(connector.next_pdu_hint().is_none()); - { - // RDCleanPath request + let written = connector.step_no_input(&mut buf)?; + let x224_pdu_len = written.size().expect("written size"); + debug_assert_eq!(x224_pdu_len, buf.filled_len()); + let x224_pdu = buf.filled().to_vec(); - let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else { - return Err(connector::general_err!("invalid connector state (send request)")); + if let Some(PreconnectionBlobPayload::VmConnect(vm_id)) = pcb { + let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { requested_protocol } = connector.state + else { + return Err(connector::general_err!("invalid connector state (wait confirm)")); }; + { + let rdcleanpath_req = ironrdp_rdcleanpath::RDCleanPathPdu::new_request( + None, + destination, + proxy_auth_token, + Some(ironrdp_vmconnect::create_pcb_payload(&vm_id)), + ); + + debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); + let rdcleanpath_req = rdcleanpath_req + .map_err(|e| connector::custom_err!("new RDCleanPath request", e))? + .to_der() + .map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?; + + framed + .write_all(&rdcleanpath_req) + .await + .map_err(|e| connector::custom_err!("couldn’t write RDCleanPath request", e))?; + } - debug_assert!(connector.next_pdu_hint().is_none()); + { + let rdcleanpath_res = framed + .read_by_hint(&RDCLEANPATH_HINT) + .await + .map_err(|e| connector::custom_err!("read RDCleanPath request", e))?; - let written = connector.step_no_input(&mut buf)?; - let x224_pdu_len = written.size().expect("written size"); - debug_assert_eq!(x224_pdu_len, buf.filled_len()); - let x224_pdu = buf.filled().to_vec(); + let rdcleanpath_res = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res) + .map_err(|e| connector::custom_err!("RDCleanPath response decode", e))?; - let rdcleanpath_req = - ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb) - .map_err(|e| connector::custom_err!("new RDCleanPath request", e))?; + debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU"); - debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); - let rdcleanpath_req = rdcleanpath_req - .to_der() - .map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?; + let server_cert_chain = match rdcleanpath_res + .into_enum() + .map_err(|e| connector::custom_err!("invalid RDCleanPath PDU", e))? + { + ironrdp_rdcleanpath::RDCleanPath::Request { .. } => { + return Err(connector::general_err!( + "received an unexpected RDCleanPath type (request)", + )); + } + ironrdp_rdcleanpath::RDCleanPath::Response { server_cert_chain, .. } => server_cert_chain, + ironrdp_rdcleanpath::RDCleanPath::Err(error) => { + return Err(connector::custom_err!("received an RDCleanPath error", error)); + } + }; - framed - .write_all(&rdcleanpath_req) - .await - .map_err(|e| connector::custom_err!("couldn’t write RDCleanPath request", e))?; - } + let server_public_key = extract_server_public_key(server_cert_chain)?; + + let should_upgrade = ironrdp_vmconnect::skip_connect_begin(); + let upgraded = ironrdp_vmconnect::force_upgrade(should_upgrade, connector, requested_protocol); + return Ok((Upgraded::VM(upgraded), server_public_key)); + } + } else { + { + let general_pcb = pcb.as_ref().map(|pcb| pcb.general()).flatten(); + // RDCleanPath request + + let rdcleanpath_req = ironrdp_rdcleanpath::RDCleanPathPdu::new_request( + Some(x224_pdu), + destination, + proxy_auth_token, + general_pcb.map(str::to_string), + ) + .map_err(|e| connector::custom_err!("new RDCleanPath request", e))?; + + debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); + let rdcleanpath_req = rdcleanpath_req + .to_der() + .map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?; - { + framed + .write_all(&rdcleanpath_req) + .await + .map_err(|e| connector::custom_err!("couldn’t write RDCleanPath request", e))?; + } // RDCleanPath response + { + let rdcleanpath_res = framed + .read_by_hint(&RDCLEANPATH_HINT) + .await + .map_err(|e| connector::custom_err!("read RDCleanPath request", e))?; - let rdcleanpath_res = framed - .read_by_hint(&RDCLEANPATH_HINT) - .await - .map_err(|e| connector::custom_err!("read RDCleanPath request", e))?; + let rdcleanpath_res = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res) + .map_err(|e| connector::custom_err!("RDCleanPath response decode", e))?; - let rdcleanpath_res = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res) - .map_err(|e| connector::custom_err!("RDCleanPath response decode", e))?; + debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU"); - debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU"); + let (x224_connection_response, server_cert_chain) = match rdcleanpath_res + .into_enum() + .map_err(|e| connector::custom_err!("invalid RDCleanPath PDU", e))? + { + ironrdp_rdcleanpath::RDCleanPath::Request { .. } => { + return Err(connector::general_err!( + "received an unexpected RDCleanPath type (request)", + )); + } + ironrdp_rdcleanpath::RDCleanPath::Response { + x224_connection_response, + server_cert_chain, + server_addr: _, + } => (x224_connection_response, server_cert_chain), + ironrdp_rdcleanpath::RDCleanPath::Err(error) => { + return Err(connector::custom_err!("received an RDCleanPath error", error)); + } + }; - let (x224_connection_response, server_cert_chain) = match rdcleanpath_res - .into_enum() - .map_err(|e| connector::custom_err!("invalid RDCleanPath PDU", e))? - { - ironrdp_rdcleanpath::RDCleanPath::Request { .. } => { - return Err(connector::general_err!( - "received an unexpected RDCleanPath type (request)", - )); - } - ironrdp_rdcleanpath::RDCleanPath::Response { - x224_connection_response, - server_cert_chain, - server_addr: _, - } => (x224_connection_response, server_cert_chain), - ironrdp_rdcleanpath::RDCleanPath::Err(error) => { - return Err(connector::custom_err!("received an RDCleanPath error", error)); - } - }; + let x224_connection_response = x224_connection_response.ok_or(connector::general_err!( + "RDCleanPath response missing X.224 connection response" + ))?; - let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { .. } = connector.state else { - return Err(connector::general_err!("invalid connector state (wait confirm)")); - }; + let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { .. } = connector.state else { + return Err(connector::general_err!("invalid connector state (wait confirm)")); + }; + + debug_assert!(connector.next_pdu_hint().is_some()); + + buf.clear(); + let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?; + + debug_assert!(written.is_nothing()); + + let server_public_key = extract_server_public_key(server_cert_chain)?; - debug_assert!(connector.next_pdu_hint().is_some()); + let should_upgrade = ironrdp_tokio::skip_connect_begin(connector); - buf.clear(); - let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?; + // At this point, proxy established the TLS session. - debug_assert!(written.is_nothing()); + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, connector); + return Ok((Upgraded::Standard(upgraded), server_public_key)); + } + } + + fn extract_server_public_key(server_cert_chain: Vec) -> ConnectorResult> { let server_cert = server_cert_chain .into_iter() .next() @@ -408,13 +507,7 @@ where .ok_or_else(|| connector::general_err!("subject public key BIT STRING is not aligned"))? .to_owned(); - let should_upgrade = ironrdp_tokio::skip_connect_begin(connector); - - // At this point, proxy established the TLS session. - - let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, connector); - - Ok((upgraded, server_public_key)) + Ok(server_public_key) } } diff --git a/crates/ironrdp-connector/src/credssp.rs b/crates/ironrdp-connector/src/credssp.rs index 1c6be266f..a334caa45 100644 --- a/crates/ironrdp-connector/src/credssp.rs +++ b/crates/ironrdp-connector/src/credssp.rs @@ -70,6 +70,7 @@ pub struct CredsspSequence { client: CredSspClient, state: CredsspState, selected_protocol: nego::SecurityProtocol, + use_vmconnect: bool, } #[derive(Debug, PartialEq)] @@ -96,6 +97,7 @@ impl CredsspSequence { server_name: ServerName, server_public_key: Vec, kerberos_config: Option, + use_vmconnect: bool, ) -> ConnectorResult<(Self, credssp::TsRequest)> { let credentials: sspi::Credentials = match &credentials { Credentials::UsernamePassword { username, password } => { @@ -163,6 +165,7 @@ impl CredsspSequence { client, state: CredsspState::Ongoing, selected_protocol: protocol, + use_vmconnect, }; let initial_request = credssp::TsRequest::default(); @@ -213,8 +216,11 @@ impl CredsspSequence { ClientState::FinalMessage(ts_request) => ( ts_request, if self.selected_protocol.contains(nego::SecurityProtocol::HYBRID_EX) { - // Don't merge this, something needs to be done here for HyperV - CredsspState::Finished + if self.use_vmconnect { + CredsspState::Finished + } else { + CredsspState::EarlyUserAuthResult + } } else { CredsspState::Finished }, diff --git a/crates/ironrdp-rdcleanpath/src/lib.rs b/crates/ironrdp-rdcleanpath/src/lib.rs index 4294b5977..56a7c0c8f 100644 --- a/crates/ironrdp-rdcleanpath/src/lib.rs +++ b/crates/ironrdp-rdcleanpath/src/lib.rs @@ -198,7 +198,7 @@ impl RDCleanPathPdu { } pub fn new_request( - x224_pdu: Vec, + x224_pdu: Option>, destination: String, proxy_auth: String, pcb: Option, @@ -208,19 +208,19 @@ impl RDCleanPathPdu { destination: Some(destination), proxy_auth: Some(proxy_auth), preconnection_blob: pcb, - x224_connection_pdu: Some(OctetString::new(x224_pdu)?), + x224_connection_pdu: x224_pdu.map(OctetString::new).transpose()?, ..Self::default() }) } pub fn new_response( server_addr: String, - x224_pdu: Vec, + x224_pdu: Option>, x509_chain: impl IntoIterator>, ) -> der::Result { Ok(Self { version: VERSION_1, - x224_connection_pdu: Some(OctetString::new(x224_pdu)?), + x224_connection_pdu: x224_pdu.map(OctetString::new).transpose()?, server_cert_chain: Some( x509_chain .into_iter() @@ -271,10 +271,10 @@ pub enum RDCleanPath { proxy_auth: String, server_auth: Option, preconnection_blob: Option, - x224_connection_request: OctetString, + x224_connection_request: Option, }, Response { - x224_connection_response: OctetString, + x224_connection_response: Option, server_cert_chain: Vec, server_addr: String, }, @@ -308,15 +308,11 @@ impl TryFrom for RDCleanPath { proxy_auth: pdu.proxy_auth.ok_or(MissingRDCleanPathField("proxy_auth"))?, server_auth: pdu.server_auth, preconnection_blob: pdu.preconnection_blob, - x224_connection_request: pdu - .x224_connection_pdu - .ok_or(MissingRDCleanPathField("x224_connection_pdu"))?, + x224_connection_request: pdu.x224_connection_pdu, } } else if let Some(server_addr) = pdu.server_addr { Self::Response { - x224_connection_response: pdu - .x224_connection_pdu - .ok_or(MissingRDCleanPathField("x224_connection_pdu"))?, + x224_connection_response: pdu.x224_connection_pdu, server_cert_chain: pdu .server_cert_chain .ok_or(MissingRDCleanPathField("server_cert_chain"))?, @@ -345,7 +341,7 @@ impl From for RDCleanPathPdu { proxy_auth: Some(proxy_auth), server_auth, preconnection_blob, - x224_connection_pdu: Some(x224_connection_request), + x224_connection_pdu: x224_connection_request, ..Default::default() }, RDCleanPath::Response { @@ -354,7 +350,7 @@ impl From for RDCleanPathPdu { server_addr, } => Self { version: VERSION_1, - x224_connection_pdu: Some(x224_connection_response), + x224_connection_pdu: x224_connection_response, server_cert_chain: Some(server_cert_chain), server_addr: Some(server_addr), ..Default::default() diff --git a/crates/ironrdp-vmconnect/src/lib.rs b/crates/ironrdp-vmconnect/src/lib.rs index af0995647..f38f58a55 100644 --- a/crates/ironrdp-vmconnect/src/lib.rs +++ b/crates/ironrdp-vmconnect/src/lib.rs @@ -7,9 +7,14 @@ use ironrdp_connector::{ ClientConnector, ClientConnectorState, ConnectionResult, ConnectorResult, Sequence, ServerName, }; use ironrdp_core::WriteBuf; +use ironrdp_pdu::nego::SecurityProtocol; use ironrdp_pdu::pcb::PcbVersion; use tracing::instrument; +pub fn create_pcb_payload(vm_id: &str) -> String { + format!("{vm_id};EnhancedMode=1") +} + #[non_exhaustive] pub struct ShouldUpgrade; @@ -47,7 +52,7 @@ where let pdu = ironrdp_pdu::pcb::PreconnectionBlob { id: 0, version: PcbVersion::V2, - v2_payload: Some(format!("{vm_id};EnhancedMode=1")), + v2_payload: Some(create_pcb_payload(vm_id)), }; let to_write = ironrdp_core::encode_vec(&pdu) @@ -69,38 +74,17 @@ pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector) -> Up } #[instrument(skip_all)] -pub async fn connect_begin_cleanpath( - framed: &mut Framed, - connector: &mut ClientConnector, - vm_id: &str, -) -> ConnectorResult -where - S: Sync + FramedRead + FramedWrite, -{ - info!("Pre-connection procedure"); - let mut buf = WriteBuf::new(); - debug_assert!(matches!( - connector.state, - ClientConnectorState::ConnectionInitiationSendRequest - )); - - let _ = connector.step(&[], &mut buf)?; - - let ClientConnectorState::ConnectionInitiationWaitConfirm { requested_protocol } = connector.state else { - return Err(ironrdp_connector::reason_err!( - "Invalid connector state", - "Expected ConnectionInitiationWaitConfirm", - )); +pub fn force_upgrade(_: ShouldUpgrade, connector: &mut ClientConnector, protocol: SecurityProtocol) -> Upgraded { + trace!("Forcing security upgrade"); + connector.state = ClientConnectorState::Credssp { + selected_protocol: protocol, }; + Upgraded +} - connector.state = ClientConnectorState::EnhancedSecurityUpgrade { - selected_protocol: requested_protocol, - }; - framed - .write_all(format!("{vm_id};EnhancedMode=1").as_bytes()) - .await - .map_err(|e| ironrdp_connector::custom_err!("Failed to write VM ID", e))?; - Ok(Upgraded) +#[instrument(skip_all)] +pub fn skip_connect_begin() -> ShouldUpgrade { + ShouldUpgrade } #[instrument(skip_all)] @@ -127,6 +111,7 @@ where server_public_key, network_client, kerberos_config, + true, // use_vmconnect ) .await?; } diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 39b773a60..5f6c378e6 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -1001,7 +1001,7 @@ where let x224_pdu = buf.filled().to_vec(); let rdcleanpath_req = - ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb) + ironrdp_rdcleanpath::RDCleanPathPdu::new_request(Some(x224_pdu), destination, proxy_auth_token, pcb) .context("new RDCleanPath request")?; debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); let rdcleanpath_req = rdcleanpath_req.to_der().context("RDCleanPath request encode")?; From 2020c76fdc53fe520e81233fbd20c9bfb7ee00da Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 4 Jun 2025 12:00:29 -0400 Subject: [PATCH 3/3] a bit clean up --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/ironrdp-web/src/session.rs | 9 ++++++++- ffi/src/credssp/mod.rs | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d64361f1..3bf0078a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4964,6 +4964,7 @@ dependencies = [ [[package]] name = "sspi" version = "0.15.6" +source = "git+https://github.com/Devolutions/sspi-rs?branch=for-temporary-vmconnect-demo#823e62a8eb6df9254c35bd70713a56c58a8dad17" dependencies = [ "async-dnssd", "async-recursion", diff --git a/Cargo.toml b/Cargo.toml index 6e318b8ad..1e6791beb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,4 +149,4 @@ opt-level = 3 # FIXME: We need to catch up with Diplomat upstream again, but this is a significant amount of work. # In the meantime, we use this forked version which fixes an undefined behavior in the code expanded by the bridge macro. diplomat = { git = "https://github.com/CBenoit/diplomat", rev = "6dc806e80162b6b39509a04a2835744236cd2396" } -sspi = { path = "../sspi-rs" } \ No newline at end of file +sspi = { git = "https://github.com/Devolutions/sspi-rs", branch = "for-temporary-vmconnect-demo" } diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 5f6c378e6..e8648a11f 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -1050,7 +1050,14 @@ where debug_assert!(connector.next_pdu_hint().is_some()); buf.clear(); - let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?; + let written = connector.step( + x224_connection_response + .ok_or(anyhow::Error::msg( + "expect x224 connection response back in RDCleanPath", + ))? + .as_bytes(), + &mut buf, + )?; debug_assert!(written.is_nothing()); diff --git a/ffi/src/credssp/mod.rs b/ffi/src/credssp/mod.rs index b912d2685..76934847b 100644 --- a/ffi/src/credssp/mod.rs +++ b/ffi/src/credssp/mod.rs @@ -54,6 +54,7 @@ pub mod ffi { server_name: &str, server_public_key: &[u8], kerbero_configs: Option<&KerberosConfig>, + use_vmconnect: bool, ) -> Result, Box> { let Some(connector) = connector.0.as_ref() else { return Err(ValueConsumedError::for_item("connector").into()); @@ -68,6 +69,7 @@ pub mod ffi { server_name.into(), server_public_key.to_owned(), kerbero_configs.map(|config| config.0.clone()), + use_vmconnect )?; Ok(Box::new(CredsspSequenceInitResult {