Skip to content

feat(client): support HyperV VM Connect #796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ironrdp-async/src/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ where
server_name,
server_public_key,
kerberos_config,
connector.config.vmconnect.is_some(),
)?;

loop {
Expand Down
1 change: 1 addition & 0 deletions crates/ironrdp-blocking/src/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ where
server_name,
server_public_key,
kerberos_config,
connector.config.vmconnect.is_some(),
)?;

loop {
Expand Down
38 changes: 22 additions & 16 deletions crates/ironrdp-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ struct Args {
/// The bitmap codecs to use (remotefx:on, ...)
#[clap(long, value_parser, num_args = 1.., value_delimiter = ',')]
codecs: Vec<String>,

/// The Virtual Machine ID for Hyper-V, used as Pre Connection Blob
#[clap(long)]
vmconnect: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think you could use a Uuid type here

}

impl Config {
Expand All @@ -253,21 +257,6 @@ impl Config {
.pipe(Destination::new)?
};

let username = if let Some(username) = args.username {
username
} else {
inquire::Text::new("Username:").prompt().context("Username prompt")?
};

let password = if let Some(password) = args.password {
password
} else {
inquire::Password::new("Password:")
.without_confirmation()
.prompt()
.context("Password prompt")?
};

let codecs: Vec<_> = args.codecs.iter().map(|s| s.as_str()).collect();
let codecs = match client_codecs_capabilities(&codecs) {
Ok(codecs) => codecs,
Expand Down Expand Up @@ -302,8 +291,24 @@ impl Config {
args.clipboard_type
};

let username = args.username.unwrap_or_else(|| {
inquire::Text::new("Username:")
.prompt()
.context("Username prompt")
.unwrap_or_else(|_| "Administrator".to_owned())
});

let password = args.password.unwrap_or_else(|| {
inquire::Password::new("Password:")
.prompt()
.context("Password prompt")
.unwrap_or_else(|_| "password".to_owned())
});

let credentials = Credentials::UsernamePassword { username, password };

let connector = connector::Config {
credentials: Credentials::UsernamePassword { username, password },
credentials,
domain: args.domain,
enable_tls: !args.no_tls,
enable_credssp: !args.no_credssp,
Expand Down Expand Up @@ -344,6 +349,7 @@ impl Config {
request_data: None,
pointer_software_rendering: true,
performance_flags: PerformanceFlags::default(),
vmconnect: args.vmconnect,
};

let rdcleanpath = args
Expand Down
39 changes: 22 additions & 17 deletions crates/ironrdp-client/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async fn connect(

let should_upgrade = ironrdp_tokio::connect_begin(&mut framed, &mut connector).await?;

debug!("TLS upgrade");
debug!(destination = ?config.destination,"TLS upgrade");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Add a debug! log before TcpStream::connect instead


// Ensure there is no leftover
let (initial_stream, leftover_bytes) = framed.into_inner();
Expand Down Expand Up @@ -292,25 +292,30 @@ where
{
// RDCleanPath request

let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else {
return Err(connector::general_err!("invalid connector state (send request)"));
};
match connector.state {
connector::ClientConnectorState::ConnectionInitiationSendRequest { .. } => {
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_req =
ironrdp_rdcleanpath::RDCleanPathPdu::new_x224_request(x224_pdu, destination, proxy_auth_token)
.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))?;
rdcleanpath_req
}
connector::ClientConnectorState::PreconnectionBlob { .. } => {}
_ => {
return Err(connector::general_err!("invalid connector state (send request)"));
}
}

debug_assert!(connector.next_pdu_hint().is_none());

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_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()
.map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?;

framed
.write_all(&rdcleanpath_req)
.await
Expand Down
112 changes: 102 additions & 10 deletions crates/ironrdp-connector/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ pub enum ClientConnectorState {
#[default]
Consumed,

ConnectionInitiationSendRequest,
PreconnectionBlob,
ConnectionInitiationSendRequest {
selected_protocol: Option<nego::SecurityProtocol>,
},
ConnectionInitiationWaitConfirm {
requested_protocol: nego::SecurityProtocol,
},
Expand Down Expand Up @@ -88,7 +91,8 @@ impl State for ClientConnectorState {
fn name(&self) -> &'static str {
match self {
Self::Consumed => "Consumed",
Self::ConnectionInitiationSendRequest => "ConnectionInitiationSendRequest",
Self::PreconnectionBlob => "PreconnectionBlob",
Self::ConnectionInitiationSendRequest { .. } => "ConnectionInitiationSendRequest",
Self::ConnectionInitiationWaitConfirm { .. } => "ConnectionInitiationWaitResponse",
Self::EnhancedSecurityUpgrade { .. } => "EnhancedSecurityUpgrade",
Self::Credssp { .. } => "Credssp",
Expand Down Expand Up @@ -132,7 +136,7 @@ impl ClientConnector {
pub fn new(config: Config, client_addr: SocketAddr) -> Self {
Self {
config,
state: ClientConnectorState::ConnectionInitiationSendRequest,
state: ClientConnectorState::PreconnectionBlob,
client_addr,
static_channels: StaticChannelSet::new(),
}
Expand Down Expand Up @@ -180,7 +184,8 @@ impl Sequence for ClientConnector {
fn next_pdu_hint(&self) -> Option<&dyn PduHint> {
match &self.state {
ClientConnectorState::Consumed => None,
ClientConnectorState::ConnectionInitiationSendRequest => None,
ClientConnectorState::PreconnectionBlob => None,
ClientConnectorState::ConnectionInitiationSendRequest { .. } => None,
ClientConnectorState::ConnectionInitiationWaitConfirm { .. } => Some(&ironrdp_pdu::X224_HINT),
ClientConnectorState::EnhancedSecurityUpgrade { .. } => None,
ClientConnectorState::Credssp { .. } => None,
Expand Down Expand Up @@ -211,12 +216,86 @@ impl Sequence for ClientConnector {
ClientConnectorState::Consumed => {
return Err(general_err!("connector sequence state is consumed (this is a bug)",))
}
ClientConnectorState::PreconnectionBlob => 'state: {
let Some(connection_id) = self.config.vmconnect.as_ref() else {
break 'state (
Written::Nothing,
ClientConnectorState::ConnectionInitiationSendRequest {
selected_protocol: None,
},
);
};

let pcb = ironrdp_pdu::pcb::PreconnectionBlob {
version: ironrdp_pdu::pcb::PcbVersion::V2,
id: 0,
// v2_payload: Some(connection_id.to_owned()),
v2_payload: Some(format!("{connection_id};EnhancedMode=1").into()),
};

debug!(message = ?pcb, "Send");

let written = ironrdp_core::encode_buf(&pcb, output).map_err(ConnectorError::encode)?;

let mut security_protocol = nego::SecurityProtocol::empty();
if self.config.enable_tls {
security_protocol.insert(nego::SecurityProtocol::SSL);
}

if self.config.enable_credssp {
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/902b090b-9cb3-4efc-92bf-ee13373371e3
// The spec is stating that `PROTOCOL_SSL` "SHOULD" also be set when using `PROTOCOL_HYBRID`.
// > PROTOCOL_HYBRID (0x00000002)
// > Credential Security Support Provider protocol (CredSSP) (section 5.4.5.2).
// > If this flag is set, then the PROTOCOL_SSL (0x00000001) flag SHOULD also be set
// > because Transport Layer Security (TLS) is a subset of CredSSP.
// However, crucially, it’s not strictly required (not "MUST").
// In fact, we purposefully choose to not set `PROTOCOL_SSL` unless `enable_winlogon` is `true`.
// This tells the server that we are not going to accept downgrading NLA to TLS security.
security_protocol.insert(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX);
}

if security_protocol.is_standard_rdp_security() {
return Err(reason_err!("Initiation", "standard RDP security is not supported",));
}

break 'state (
Written::from_size(written)?,
ClientConnectorState::EnhancedSecurityUpgrade {
selected_protocol: security_protocol,
},
);
}
//== Connection Initiation ==//
// Exchange supported security protocols and a few other connection flags.
ClientConnectorState::ConnectionInitiationSendRequest => {
ClientConnectorState::ConnectionInitiationSendRequest { selected_protocol } => 'state: {
debug!("Connection Initiation");

if self.config.vmconnect.is_some() {
let selected_protocol = selected_protocol.ok_or(reason_err!(
"Initiation",
"VMConnect requires a selected protocol at ConnectionInitiationSendRequest state",
))?;

let connection_request = nego::ConnectionRequest {
nego_data: None,
flags: nego::RequestFlags::empty(),
protocol: selected_protocol,
};

debug!(message = ?connection_request, "Send");

let written =
ironrdp_core::encode_buf(&X224(connection_request), output).map_err(ConnectorError::encode)?;

break 'state (
Written::from_size(written)?,
ClientConnectorState::ConnectionInitiationWaitConfirm {
requested_protocol: selected_protocol,
},
);
}

let mut security_protocol = nego::SecurityProtocol::empty();

if self.config.enable_tls {
Expand Down Expand Up @@ -289,7 +368,10 @@ impl Sequence for ClientConnector {

(
Written::Nothing,
ClientConnectorState::EnhancedSecurityUpgrade { selected_protocol },
match self.config.vmconnect.is_some() {
false => ClientConnectorState::EnhancedSecurityUpgrade { selected_protocol },
true => ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol },
},
)
}

Expand All @@ -311,10 +393,20 @@ impl Sequence for ClientConnector {
}

//== CredSSP ==//
ClientConnectorState::Credssp { selected_protocol } => (
Written::Nothing,
ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol },
),
ClientConnectorState::Credssp { selected_protocol } => 'state: {
if self.config.vmconnect.is_some() {
break 'state (
Written::Nothing,
ClientConnectorState::ConnectionInitiationSendRequest {
selected_protocol: Some(selected_protocol),
},
);
}
(
Written::Nothing,
ClientConnectorState::BasicSettingsExchangeSendInitial { selected_protocol },
)
}

//== Basic Settings Exchange ==//
// Exchange basic settings including Core Data, Security Data and Network Data.
Expand Down
2 changes: 2 additions & 0 deletions crates/ironrdp-connector/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ pub struct Config {
pub no_server_pointer: bool,
pub pointer_software_rendering: bool,
pub performance_flags: PerformanceFlags,

pub vmconnect: Option<String>,
Comment on lines +190 to +191
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: I don’t think we should expose a vmconnect option. Also, since the PCB is something outside of the RDP protocol itself, let’s not handle it in the connector at all.

}

ironrdp_core::assert_impl!(Config: Send, Sync);
Expand Down
6 changes: 6 additions & 0 deletions crates/ironrdp-core/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,12 @@ impl<'a> WriteCursor<'a> {
self.pos
}

/// Returns the number of bytes written.
#[inline]
pub const fn bytes_written(&self) -> usize {
self.pos
}
Comment on lines +581 to +585
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Remove this method. You can use pos() instead which is the same thing. Also, strictly speaking, it’s not necessarily the number of bytes written because it’s possible to "rewind" the cursor.


/// Write an array of bytes to the buffer.
#[inline]
#[track_caller]
Expand Down
5 changes: 5 additions & 0 deletions crates/ironrdp-pdu/src/rdp/client_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ impl Encode for ExtendedClientOptionalInfo {
dst.write_array(reconnect_cookie);
}

dst.write_u16(0); // reserved1
dst.write_u16(0); // reserved2
Comment on lines +316 to +317
Copy link
Member

@CBenoit CBenoit Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Did you fix a bug? Could you elaborate on that? Please, open a separate PR dedicated to this with a fix commit type.


Ok(())
}

Expand All @@ -336,6 +339,8 @@ impl Encode for ExtendedClientOptionalInfo {
size += RECONNECT_COOKIE_LENGTH_SIZE + RECONNECT_COOKIE_LEN;
}

size += 2 * 2; // reserved1 and reserved2

size
}
}
Expand Down
Loading
Loading