Skip to content

feat(dgw): support vmconnect in RDCleanPath #1372

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

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 0 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion devolutions-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ job-queue = { path = "../crates/job-queue" }
job-queue-libsql = { path = "../crates/job-queue-libsql" }
ironrdp-pdu = { version = "0.5", features = ["std"] }
ironrdp-core = { version = "0.1", features = ["std"] }
ironrdp-rdcleanpath = "0.1"
ironrdp-tokio = "0.5"
ironrdp-rdcleanpath = { path = "C:\\dev\\IronRDP\\crates\\ironrdp-rdcleanpath" }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

will replace with commit/new version later

ironrdp-connector = { version = "0.5" }
ironrdp-acceptor = { version = "0.5" }
ceviche = "0.6.1"
Expand Down
92 changes: 66 additions & 26 deletions devolutions-gateway/src/rd_clean_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,7 @@ async fn read_cleanpath_pdu(mut stream: impl AsyncRead + Unpin + Send) -> io::Re
std::cmp::Ordering::Less => {}
std::cmp::Ordering::Equal => break,
std::cmp::Ordering::Greater => {
return Err(io::Error::new(
ErrorKind::Other,
"no leftover is expected when reading cleanpath PDU",
));
return Err(io::Error::other("no leftover is expected when reading cleanpath PDU"));
}
}
}
Expand Down Expand Up @@ -164,7 +161,7 @@ struct CleanPathResult {
destination: TargetAddr,
server_addr: SocketAddr,
server_stream: tokio_rustls::client::TlsStream<tokio::net::TcpStream>,
x224_rsp: Vec<u8>,
x224_rsp: Option<Vec<u8>>,
}

async fn process_cleanpath(
Expand Down Expand Up @@ -234,31 +231,74 @@ async fn process_cleanpath(
debug!(%selected_target, "Connected to destination server");
span.record("target", selected_target.to_string());

// Send preconnection blob if applicable
if let Some(pcb) = cleanpath_pdu.preconnection_blob {
server_stream.write_all(pcb.as_bytes()).await?;
}
// Preconnection Blob (PCB) is currently used for Hyper-V VMs almost exclusively in practice.
// However, we still leave space for future extensions of usages of PCB.
//
// Connection sequence with Hyper-V VMs (PCB included and X224 connection request is not present):
// ┌───────────────────────┐ ┌───────────────────────────────────────────────────────────────┐
// │ handled by │ │ handled by IronRDP client │
// │ Gateway │ │ │
// └───────────────────────┘ └───────────────────────────────────────────────────────────────┘
// │ PCB → TLS handshake │ → │ CredSSP → X224 connection request → X224 connection response │
// └───────────────────────┘ └───────────────────────────────────────────────────────────────┘
//
// Connection sequence without Hyper-V VMs (PCB optional):
// ┌───────────────────────────────────────────────────────────────┐ ┌───────────────────────┐
// │ handled by Gateway │ │ handled by IronRDP │
// │ │ │ client │
// └───────────────────────────────────────────────────────────────┘ └───────────────────────┘
// │ PCB → X224 connection request → X224 connection response → TLS| │ → CredSSP → ... │
// └───────────────────────────────────────────────────────────────┘ └───────────────────────┘
//
// Summary:
// - With PCB but not X224 connection request: Gateway handles (1) sending PCB/VmConnectID, (2) TLS handshake, then leaves CredSSP
// and X224 connection request/response to IronRDP client.
// - With PCB and X224 connection request: Gateway handles (1) sending PCB/VmConnectID, (2) X224 connection request, (3) X224 connection response, (4) TLS handshake,
// then leaves CredSSP to IronRDP client.
// - Without PCB: In this case, X224 MUST be present! Gateway handles (1) X224 connection request, (2) X224 connection response, (3) TLS handshake, then leaves CredSSP to IronRDP client.
let pcb_bytes = match (&cleanpath_pdu.preconnection_blob, &cleanpath_pdu.x224_connection_pdu) {
(None, None) => {
return Err(CleanPathError::BadRequest(anyhow::anyhow!(
"RDCleanPath PDU is missing both preconnection blob and X224 connection PDU"
)));
}
(None, Some(_)) => None, // no preconnection blob to send
(Some(general_pcb), Some(_)) => Some(general_pcb),
(Some(vmconnect), None) => Some(vmconnect),
};

// Send X224 connection request
let x224_req = cleanpath_pdu
.x224_connection_pdu
.context("request is missing X224 connection PDU")
.map_err(CleanPathError::BadRequest)?;
server_stream.write_all(x224_req.as_bytes()).await?;
if let Some(pcb_bytes) = pcb_bytes {
let pcb = ironrdp_pdu::pcb::PreconnectionBlob {
version: ironrdp_pdu::pcb::PcbVersion::V2,
id: 0,
v2_payload: Some(pcb_bytes.to_owned()),
};

// Receive server X224 connection response
let encoded = ironrdp_core::encode_vec(&pcb)
.context("failed to encode preconnection blob")
.map_err(CleanPathError::BadRequest)?;

trace!("Receiving X224 response");
server_stream.write_all(&encoded).await?;
}

let x224_rsp = read_x224_response(&mut server_stream)
.await
.with_context(|| format!("read X224 response from {selected_target}"))
.map_err(CleanPathError::BadRequest)?;
// Send X224 connection request if present
let x224_rsp = if let Some(x224_connection_pdu) = &cleanpath_pdu.x224_connection_pdu {
server_stream.write_all(x224_connection_pdu.as_bytes()).await?;
debug!("X224 connection request sent");

trace!("Establishing TLS connection with server");
let x224_rsp = read_x224_response(&mut server_stream)
.await
.with_context(|| format!("read X224 response from {selected_target}"))
.map_err(CleanPathError::BadRequest)?;
trace!("Receiving X224 response");

// Establish TLS connection with server
Some(x224_rsp)
} else {
None
};

// Establish TLS connection
trace!("Establishing TLS connection with server");
let server_stream = crate::tls::connect(selected_target.host().to_owned(), server_stream)
.await
.map_err(|source| CleanPathError::TlsHandshake {
Expand Down Expand Up @@ -295,7 +335,7 @@ pub async fn handle(
.await
.context("couldn’t read clean cleanpath PDU")?;

trace!("Processing RDCleanPath");
trace!(RDCleanPath = ?cleanpath_pdu,"Processing RDCleanPath");

let CleanPathResult {
claims,
Expand Down Expand Up @@ -337,7 +377,7 @@ pub async fn handle(
trace!("Sending RDCleanPath response");

let rdcleanpath_rsp = RDCleanPathPdu::new_response(server_addr.to_string(), x224_rsp, x509_chain)
.map_err(|e| anyhow::anyhow!("couldn’t build RDCleanPath response: {e}"))?;
.context("couldn’t build RDCleanPath response")?;

send_clean_path_response(&mut client_stream, &rdcleanpath_rsp).await?;

Expand Down Expand Up @@ -456,7 +496,7 @@ enum WsaError {
WSAESTALE = 10070,
WSAEREMOTE = 10071,
WSASYSNOTREADY = 10091,
WSAVERNOTSUPPORTED = 10092,
WSAVERNOT_SUPPORTED = 10092,
WSANOTINITIALISED = 10093,
WSAEDISCON = 10101,
WSAENOMORE = 10102,
Expand Down
1 change: 1 addition & 0 deletions devolutions-gateway/src/rdp_pcb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ fn decode_pcb(buf: &[u8]) -> Result<Option<(PreconnectionBlob, usize)>, io::Erro
Ok(pcb) => {
let pdu_size = ironrdp_core::size(&pcb);
let read_len = cursor.pos();
debug!(pdu_size, read_len, "read preconnection blob",);

// NOTE: sanity check (reporting the wrong number will corrupt the communication)
if read_len != pdu_size {
Expand Down
2 changes: 1 addition & 1 deletion devolutions-gateway/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub fn build_server_config(cert_source: CertificateSource) -> anyhow::Result<rus
} => {
let first_certificate = certificates.first().context("empty certificate list")?;

if let Ok(report) = check_certificate_now(&first_certificate) {
if let Ok(report) = check_certificate_now(first_certificate) {
if report.issues.intersects(
CertIssues::MISSING_SERVER_AUTH_EXTENDED_KEY_USAGE | CertIssues::MISSING_SUBJECT_ALT_NAME,
) {
Expand Down
Loading