Skip to content

Conversation

@AlexMartigny
Copy link

  • Thread credential_store through RDCleanPath API handlers
  • Add handle_with_credential_injection function for CredSSP flow
  • Expose helper functions for CredSSP and TLS key extraction
  • Enable early credential detection in WebSocket RDCleanPath path

@AlexMartigny AlexMartigny requested a review from CBenoit December 8, 2025 16:28
@github-actions
Copy link

github-actions bot commented Dec 8, 2025

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

@AlexMartigny AlexMartigny force-pushed the dev/rdcleanpath-credential-injection branch from f038618 to 36c71f7 Compare December 8, 2025 16:42
@AlexMartigny AlexMartigny added publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) and removed publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) labels Dec 8, 2025
@github-actions
Copy link

github-actions bot commented Dec 8, 2025

Maintainer action requested

Author requested libraries to be published.

cc @CBenoit

@github-actions
Copy link

github-actions bot commented Dec 8, 2025

Maintainer action requested

Author requested a new release to be cut.

cc @CBenoit

@AlexMartigny AlexMartigny force-pushed the dev/rdcleanpath-credential-injection branch from 36c71f7 to 01a6ab7 Compare December 8, 2025 17:04
- Thread credential_store through RDCleanPath API handlers
- Add handle_with_credential_injection function for CredSSP flow
- Expose helper functions for CredSSP and TLS key extraction
- Enable early credential detection in WebSocket RDCleanPath path
@AlexMartigny AlexMartigny force-pushed the dev/rdcleanpath-credential-injection branch from 01a6ab7 to c701ecc Compare December 8, 2025 17:09
@AlexMartigny AlexMartigny removed the release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) label Dec 8, 2025
@CBenoit CBenoit added the release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) label Dec 9, 2025
@github-actions
Copy link

github-actions bot commented Dec 9, 2025

Maintainer action requested

Author requested a new release to be cut.

cc @CBenoit


#[instrument(name = "server_credssp", level = "debug", ret, skip_all)]
async fn perform_credssp_with_server<S>(
pub async fn perform_credssp_with_server<S>(
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Should not be made pub. If absolutely necessary use pub(crate) and keep it an implementation detail. Same for all the functions in this module.

#[instrument("fwd", skip_all, fields(session_id = field::Empty, target = field::Empty))]
pub async fn handle(
mut client_stream: impl AsyncRead + AsyncWrite + Unpin + Send,
mut client_stream: impl AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Verify we use the smallest set of traits possible here.

Comment on lines +308 to +359
// Run normal RDCleanPath flow (this will handle server-side TLS and get certs)
let CleanPathResult {
destination,
server_addr,
server_stream,
x224_rsp,
..
} = process_cleanpath(
cleanpath_pdu,
client_addr,
&conf,
token_cache,
jrl,
active_recordings,
&sessions,
&CredentialStoreHandle::new(), // Dummy, not used in process_cleanpath
)
.await
.map_err(|e| anyhow::anyhow!("RDCleanPath processing failed: {}", e))?;

// Extract server security protocol from X224 response (before x224_rsp is moved)
let x224_confirm: ironrdp_pdu::x224::X224<nego::ConnectionConfirm> =
ironrdp_core::decode(&x224_rsp).context("decode X224 connection confirm")?;
let server_security_protocol = match &x224_confirm.0 {
nego::ConnectionConfirm::Response { protocol, .. } => {
if !protocol.intersects(nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX) {
anyhow::bail!(
"server selected security protocol {protocol}, which is not supported for credential injection"
);
}
*protocol
}
nego::ConnectionConfirm::Failure { code } => {
anyhow::bail!("RDP session initiation failed with code {code}");
}
};

// Send RDCleanPath response to client (includes server certs)
let x509_chain = server_stream
.get_ref()
.1
.peer_certificates()
.context("no peer certificate found in TLS transport")?
.iter()
.map(|cert| cert.to_vec());

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}"))?;

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

Choose a reason for hiding this comment

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

@copilot Possibly duplicate code here. Double check that.

Comment on lines +369 to +452
// Extract server public key from TLS stream
let server_public_key =
crate::rdp_proxy::extract_tls_server_public_key(&server_stream).context("extract server TLS public key")?;

// Wrap streams in TokioFramed for CredSSP
let mut client_framed = ironrdp_tokio::TokioFramed::new(client_stream);
let mut server_framed = ironrdp_tokio::TokioFramed::new(server_stream);

// Use HYBRID_EX for client (web clients typically use this)
let client_security_protocol = nego::SecurityProtocol::HYBRID_EX;

// Perform CredSSP MITM (in parallel)
// Note: Client expects server's public key (since we sent server certs in RDCleanPath response)
let client_credssp_fut = crate::rdp_proxy::perform_credssp_with_client(
&mut client_framed,
client_addr.ip(),
server_public_key.clone(),
client_security_protocol,
&credential_mapping.proxy,
);

let server_credssp_fut = crate::rdp_proxy::perform_credssp_with_server(
&mut server_framed,
destination.host().to_owned(),
server_public_key,
server_security_protocol,
&credential_mapping.target,
);

let (client_res, server_res) = tokio::join!(client_credssp_fut, server_credssp_fut);
client_res.context("CredSSP with client failed")?;
server_res.context("CredSSP with server failed")?;

info!("CredSSP MITM completed successfully");

// Extract streams and any leftover bytes
let (mut client_stream, client_leftover) = client_framed.into_inner();
let (mut server_stream, server_leftover) = server_framed.into_inner();

// Forward any leftover bytes
if !server_leftover.is_empty() {
client_stream
.write_all(&server_leftover)
.await
.context("write server leftover to client")?;
}
if !client_leftover.is_empty() {
server_stream
.write_all(&client_leftover)
.await
.context("write client leftover to server")?;
}

info!("RDP-TLS forwarding (credential injection)");

// Build SessionInfo for forwarding
let session_info = SessionInfo::builder()
.id(claims.jet_aid)
.application_protocol(claims.jet_ap)
.details(ConnectionModeDetails::Fwd {
destination_host: destination.clone(),
})
.time_to_live(claims.jet_ttl)
.recording_policy(claims.jet_rec)
.filtering_policy(claims.jet_flt)
.build();

let disconnect_interest = DisconnectInterest::from_reconnection_policy(claims.jet_reuse);

// Plain forwarding for now
Proxy::builder()
.conf(conf)
.session_info(session_info)
.address_a(client_addr)
.transport_a(client_stream)
.address_b(server_addr)
.transport_b(server_stream)
.sessions(sessions)
.subscriber_tx(subscriber_tx)
.disconnect_interest(disconnect_interest)
.build()
.select_dissector_and_forward()
.await
.context("proxy failed")
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Can possibly be refactored into the rdp_proxy module. Double check that.

Comment on lines +476 to +503
// Early credential detection: check if we should use RdpProxy instead
let token = cleanpath_pdu
.proxy_auth
.as_deref()
.ok_or_else(|| anyhow::anyhow!("missing token in RDCleanPath PDU"))?;

if let Some(entry) = crate::token::extract_jti(token)
.ok()
.and_then(|token_id| credential_store.get(token_id))
.filter(|entry| entry.mapping.is_some())
{
// Credentials found! Switch to RdpProxy for credential injection
info!("Switching to RdpProxy for credential injection (WebSocket)");

return handle_with_credential_injection(
client_stream,
client_addr,
conf,
token_cache,
jrl,
sessions,
subscriber_tx,
active_recordings,
cleanpath_pdu,
entry,
)
.await;
}
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Since we already extract the token here, verify if we can avoid re-extracting it later. Avoid doing the job twice.

Copy link
Contributor

Copilot AI commented Dec 9, 2025

@CBenoit I've opened a new pull request, #1618, to work on those changes. Once the pull request is ready, I'll request review from you.

jrl: &CurrentJrl,
active_recordings: &ActiveRecordings,
sessions: &SessionMessageSender,
_credential_store: &CredentialStoreHandle,
Copy link
Member

Choose a reason for hiding this comment

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

issue: Unused parameter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

Development

Successfully merging this pull request may close these issues.

3 participants