@@ -3,6 +3,7 @@ use std::net::SocketAddr;
33use std:: sync:: Arc ;
44
55use crate :: config:: Conf ;
6+ use crate :: credential:: CredentialStoreHandle ;
67use crate :: proxy:: Proxy ;
78use crate :: recording:: ActiveRecordings ;
89use crate :: session:: { ConnectionModeDetails , DisconnectInterest , DisconnectedInfo , SessionInfo , SessionMessageSender } ;
@@ -11,6 +12,7 @@ use crate::target_addr::TargetAddr;
1112use crate :: token:: { AssociationTokenClaims , CurrentJrl , TokenCache , TokenError } ;
1213
1314use anyhow:: Context as _;
15+ use ironrdp_pdu:: nego;
1416use ironrdp_rdcleanpath:: RDCleanPathPdu ;
1517use tap:: prelude:: * ;
1618use thiserror:: Error ;
@@ -172,6 +174,7 @@ async fn process_cleanpath(
172174 jrl : & CurrentJrl ,
173175 active_recordings : & ActiveRecordings ,
174176 sessions : & SessionMessageSender ,
177+ _credential_store : & CredentialStoreHandle ,
175178) -> Result < CleanPathResult , CleanPathError > {
176179 use crate :: utils;
177180
@@ -272,25 +275,248 @@ async fn process_cleanpath(
272275 } )
273276}
274277
278+ /// Handle RDP connection with credential injection via CredSSP MITM
279+ #[ allow( clippy:: too_many_arguments) ]
280+ async fn handle_with_credential_injection (
281+ mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send + Sync + ' static ,
282+ client_addr : SocketAddr ,
283+ conf : Arc < Conf > ,
284+ token_cache : & TokenCache ,
285+ jrl : & CurrentJrl ,
286+ sessions : SessionMessageSender ,
287+ subscriber_tx : SubscriberSender ,
288+ active_recordings : & ActiveRecordings ,
289+ cleanpath_pdu : RDCleanPathPdu ,
290+ _credential_entry : crate :: credential:: ArcCredentialEntry ,
291+ ) -> anyhow:: Result < ( ) > {
292+ let token = cleanpath_pdu
293+ . proxy_auth
294+ . as_ref ( )
295+ . context ( "missing token" ) ?;
296+
297+ // Authorize the token
298+ let claims = authorize (
299+ client_addr,
300+ token,
301+ & conf,
302+ token_cache,
303+ jrl,
304+ active_recordings,
305+ None ,
306+ )
307+ . map_err ( |e| anyhow:: anyhow!( "authorization failed: {}" , e) ) ?;
308+
309+ let crate :: token:: ConnectionMode :: Fwd { targets : _ } = claims. jet_cm else {
310+ anyhow:: bail!( "unexpected connection mode" ) ;
311+ } ;
312+
313+ let span = tracing:: Span :: current ( ) ;
314+ span. record ( "session_id" , claims. jet_aid . to_string ( ) ) ;
315+
316+ info ! ( "Credential injection: performing CredSSP MITM" ) ;
317+
318+ // Run normal RDCleanPath flow (this will handle server-side TLS and get certs)
319+ let CleanPathResult {
320+ destination,
321+ server_addr,
322+ server_stream,
323+ x224_rsp,
324+ ..
325+ } = process_cleanpath (
326+ cleanpath_pdu,
327+ client_addr,
328+ & conf,
329+ token_cache,
330+ jrl,
331+ active_recordings,
332+ & sessions,
333+ & CredentialStoreHandle :: new ( ) , // Dummy, not used in process_cleanpath
334+ )
335+ . await
336+ . map_err ( |e| anyhow:: anyhow!( "RDCleanPath processing failed: {}" , e) ) ?;
337+
338+ // Extract server security protocol from X224 response (before x224_rsp is moved)
339+ let x224_confirm: ironrdp_pdu:: x224:: X224 < nego:: ConnectionConfirm > = ironrdp_core:: decode ( & x224_rsp)
340+ . context ( "decode X224 connection confirm" ) ?;
341+ let server_security_protocol = match & x224_confirm. 0 {
342+ nego:: ConnectionConfirm :: Response { protocol, .. } => {
343+ if !protocol. intersects ( nego:: SecurityProtocol :: HYBRID | nego:: SecurityProtocol :: HYBRID_EX ) {
344+ anyhow:: bail!(
345+ "server selected security protocol {protocol}, which is not supported for credential injection"
346+ ) ;
347+ }
348+ * protocol
349+ }
350+ nego:: ConnectionConfirm :: Failure { code } => {
351+ anyhow:: bail!( "RDP session initiation failed with code {code}" ) ;
352+ }
353+ } ;
354+
355+ // Send RDCleanPath response to client (includes server certs)
356+ let x509_chain = server_stream
357+ . get_ref ( )
358+ . 1
359+ . peer_certificates ( )
360+ . context ( "no peer certificate found in TLS transport" ) ?
361+ . iter ( )
362+ . map ( |cert| cert. to_vec ( ) ) ;
363+
364+ trace ! ( "Sending RDCleanPath response" ) ;
365+
366+ let rdcleanpath_rsp = RDCleanPathPdu :: new_response ( server_addr. to_string ( ) , x224_rsp, x509_chain)
367+ . map_err ( |e| anyhow:: anyhow!( "couldn't build RDCleanPath response: {e}" ) ) ?;
368+
369+ send_clean_path_response ( & mut client_stream, & rdcleanpath_rsp) . await ?;
370+
371+ info ! ( "RDCleanPath response sent, now performing CredSSP MITM" ) ;
372+
373+ // Get TLS configuration for CredSSP
374+ let tls_conf = conf
375+ . tls
376+ . as_ref ( )
377+ . context ( "TLS required for credential injection" ) ?;
378+
379+ // Get credential mapping
380+ let credential_mapping = _credential_entry
381+ . mapping
382+ . as_ref ( )
383+ . context ( "no credential mapping" ) ?;
384+
385+ // Extract server public key from TLS stream
386+ let server_public_key = crate :: rdp_proxy:: extract_tls_server_public_key ( & server_stream)
387+ . context ( "extract server TLS public key" ) ?;
388+
389+ // Wrap streams in TokioFramed for CredSSP
390+ let mut client_framed = ironrdp_tokio:: TokioFramed :: new ( client_stream) ;
391+ let mut server_framed = ironrdp_tokio:: TokioFramed :: new ( server_stream) ;
392+
393+ // Use HYBRID_EX for client (web clients typically use this)
394+ let client_security_protocol = nego:: SecurityProtocol :: HYBRID_EX ;
395+
396+ // Perform CredSSP MITM (in parallel)
397+ // Note: Client expects server's public key (since we sent server certs in RDCleanPath response)
398+ let client_credssp_fut = crate :: rdp_proxy:: perform_credssp_with_client (
399+ & mut client_framed,
400+ client_addr. ip ( ) ,
401+ server_public_key. clone ( ) ,
402+ client_security_protocol,
403+ & credential_mapping. proxy ,
404+ ) ;
405+
406+ let server_credssp_fut = crate :: rdp_proxy:: perform_credssp_with_server (
407+ & mut server_framed,
408+ destination. host ( ) . to_owned ( ) ,
409+ server_public_key,
410+ server_security_protocol,
411+ & credential_mapping. target ,
412+ ) ;
413+
414+ let ( client_res, server_res) = tokio:: join!( client_credssp_fut, server_credssp_fut) ;
415+ client_res. context ( "CredSSP with client failed" ) ?;
416+ server_res. context ( "CredSSP with server failed" ) ?;
417+
418+ info ! ( "CredSSP MITM completed successfully" ) ;
419+
420+ // Extract streams and any leftover bytes
421+ let ( mut client_stream, client_leftover) = client_framed. into_inner ( ) ;
422+ let ( mut server_stream, server_leftover) = server_framed. into_inner ( ) ;
423+
424+ // Forward any leftover bytes
425+ if !server_leftover. is_empty ( ) {
426+ client_stream
427+ . write_all ( & server_leftover)
428+ . await
429+ . context ( "write server leftover to client" ) ?;
430+ }
431+ if !client_leftover. is_empty ( ) {
432+ server_stream
433+ . write_all ( & client_leftover)
434+ . await
435+ . context ( "write client leftover to server" ) ?;
436+ }
437+
438+ info ! ( "RDP-TLS forwarding (credential injection)" ) ;
439+
440+ // Build SessionInfo for forwarding
441+ let session_info = SessionInfo :: builder ( )
442+ . id ( claims. jet_aid )
443+ . application_protocol ( claims. jet_ap )
444+ . details ( ConnectionModeDetails :: Fwd {
445+ destination_host : destination. clone ( ) ,
446+ } )
447+ . time_to_live ( claims. jet_ttl )
448+ . recording_policy ( claims. jet_rec )
449+ . filtering_policy ( claims. jet_flt )
450+ . build ( ) ;
451+
452+ let disconnect_interest = DisconnectInterest :: from_reconnection_policy ( claims. jet_reuse ) ;
453+
454+ // Plain forwarding for now
455+ Proxy :: builder ( )
456+ . conf ( conf)
457+ . session_info ( session_info)
458+ . address_a ( client_addr)
459+ . transport_a ( client_stream)
460+ . address_b ( server_addr)
461+ . transport_b ( server_stream)
462+ . sessions ( sessions)
463+ . subscriber_tx ( subscriber_tx)
464+ . disconnect_interest ( disconnect_interest)
465+ . build ( )
466+ . select_dissector_and_forward ( )
467+ . await
468+ . context ( "proxy failed" )
469+ }
470+
275471#[ allow( clippy:: too_many_arguments) ]
276472#[ instrument( "fwd" , skip_all, fields( session_id = field:: Empty , target = field:: Empty ) ) ]
277473pub async fn handle (
278- mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send ,
474+ mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send + Sync + ' static ,
279475 client_addr : SocketAddr ,
280476 conf : Arc < Conf > ,
281477 token_cache : & TokenCache ,
282478 jrl : & CurrentJrl ,
283479 sessions : SessionMessageSender ,
284480 subscriber_tx : SubscriberSender ,
285481 active_recordings : & ActiveRecordings ,
482+ credential_store : & CredentialStoreHandle ,
286483) -> anyhow:: Result < ( ) > {
287484 // Special handshake of our RDP extension
288485
289486 trace ! ( "Reading RDCleanPath" ) ;
290487
291488 let cleanpath_pdu = read_cleanpath_pdu ( & mut client_stream)
292489 . await
293- . context ( "couldn’t read clean cleanpath PDU" ) ?;
490+ . context ( "couldn't read clean cleanpath PDU" ) ?;
491+
492+ // Early credential detection: check if we should use RdpProxy instead
493+ let token = cleanpath_pdu
494+ . proxy_auth
495+ . as_deref ( )
496+ . ok_or_else ( || anyhow:: anyhow!( "missing token in RDCleanPath PDU" ) ) ?;
497+
498+ if let Ok ( token_id) = crate :: token:: extract_jti ( token) {
499+ if let Some ( entry) = credential_store. get ( token_id) {
500+ if entry. mapping . is_some ( ) {
501+ // Credentials found! Switch to RdpProxy for credential injection
502+ info ! ( "Switching to RdpProxy for credential injection (WebSocket)" ) ;
503+
504+ return handle_with_credential_injection (
505+ client_stream,
506+ client_addr,
507+ conf,
508+ token_cache,
509+ jrl,
510+ sessions,
511+ subscriber_tx,
512+ active_recordings,
513+ cleanpath_pdu,
514+ entry,
515+ )
516+ . await ;
517+ }
518+ }
519+ }
294520
295521 trace ! ( "Processing RDCleanPath" ) ;
296522
@@ -308,6 +534,7 @@ pub async fn handle(
308534 jrl,
309535 active_recordings,
310536 & sessions,
537+ credential_store,
311538 )
312539 . await
313540 {
0 commit comments