@@ -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,231 @@ 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. proxy_auth . as_ref ( ) . context ( "missing token" ) ?;
293+
294+ // Authorize the token
295+ let claims = authorize ( client_addr, token, & conf, token_cache, jrl, active_recordings, None )
296+ . map_err ( |e| anyhow:: anyhow!( "authorization failed: {}" , e) ) ?;
297+
298+ let crate :: token:: ConnectionMode :: Fwd { targets : _ } = claims. jet_cm else {
299+ anyhow:: bail!( "unexpected connection mode" ) ;
300+ } ;
301+
302+ let span = tracing:: Span :: current ( ) ;
303+ span. record ( "session_id" , claims. jet_aid . to_string ( ) ) ;
304+
305+ info ! ( "Credential injection: performing CredSSP MITM" ) ;
306+
307+ // Run normal RDCleanPath flow (this will handle server-side TLS and get certs)
308+ let CleanPathResult {
309+ destination,
310+ server_addr,
311+ server_stream,
312+ x224_rsp,
313+ ..
314+ } = process_cleanpath (
315+ cleanpath_pdu,
316+ client_addr,
317+ & conf,
318+ token_cache,
319+ jrl,
320+ active_recordings,
321+ & sessions,
322+ & CredentialStoreHandle :: new ( ) , // Dummy, not used in process_cleanpath
323+ )
324+ . await
325+ . map_err ( |e| anyhow:: anyhow!( "RDCleanPath processing failed: {}" , e) ) ?;
326+
327+ // Extract server security protocol from X224 response (before x224_rsp is moved)
328+ let x224_confirm: ironrdp_pdu:: x224:: X224 < nego:: ConnectionConfirm > =
329+ ironrdp_core:: decode ( & x224_rsp) . context ( "decode X224 connection confirm" ) ?;
330+ let server_security_protocol = match & x224_confirm. 0 {
331+ nego:: ConnectionConfirm :: Response { protocol, .. } => {
332+ if !protocol. intersects ( nego:: SecurityProtocol :: HYBRID | nego:: SecurityProtocol :: HYBRID_EX ) {
333+ anyhow:: bail!(
334+ "server selected security protocol {protocol}, which is not supported for credential injection"
335+ ) ;
336+ }
337+ * protocol
338+ }
339+ nego:: ConnectionConfirm :: Failure { code } => {
340+ anyhow:: bail!( "RDP session initiation failed with code {code}" ) ;
341+ }
342+ } ;
343+
344+ // Send RDCleanPath response to client (includes server certs)
345+ let x509_chain = server_stream
346+ . get_ref ( )
347+ . 1
348+ . peer_certificates ( )
349+ . context ( "no peer certificate found in TLS transport" ) ?
350+ . iter ( )
351+ . map ( |cert| cert. to_vec ( ) ) ;
352+
353+ trace ! ( "Sending RDCleanPath response" ) ;
354+
355+ let rdcleanpath_rsp = RDCleanPathPdu :: new_response ( server_addr. to_string ( ) , x224_rsp, x509_chain)
356+ . map_err ( |e| anyhow:: anyhow!( "couldn't build RDCleanPath response: {e}" ) ) ?;
357+
358+ send_clean_path_response ( & mut client_stream, & rdcleanpath_rsp) . await ?;
359+
360+ info ! ( "RDCleanPath response sent, now performing CredSSP MITM" ) ;
361+
362+ // Get TLS configuration for CredSSP
363+ let tls_conf = conf. tls . as_ref ( ) . context ( "TLS required for credential injection" ) ?;
364+
365+ // Get credential mapping
366+ let credential_mapping = _credential_entry. mapping . as_ref ( ) . context ( "no credential mapping" ) ?;
367+
368+ // Extract server public key from TLS stream
369+ let server_public_key =
370+ crate :: rdp_proxy:: extract_tls_server_public_key ( & server_stream) . context ( "extract server TLS public key" ) ?;
371+
372+ // Wrap streams in TokioFramed for CredSSP
373+ let mut client_framed = ironrdp_tokio:: TokioFramed :: new ( client_stream) ;
374+ let mut server_framed = ironrdp_tokio:: TokioFramed :: new ( server_stream) ;
375+
376+ // Use HYBRID_EX for client (web clients typically use this)
377+ let client_security_protocol = nego:: SecurityProtocol :: HYBRID_EX ;
378+
379+ // Perform CredSSP MITM (in parallel)
380+ // Note: Client expects server's public key (since we sent server certs in RDCleanPath response)
381+ let client_credssp_fut = crate :: rdp_proxy:: perform_credssp_with_client (
382+ & mut client_framed,
383+ client_addr. ip ( ) ,
384+ server_public_key. clone ( ) ,
385+ client_security_protocol,
386+ & credential_mapping. proxy ,
387+ ) ;
388+
389+ let server_credssp_fut = crate :: rdp_proxy:: perform_credssp_with_server (
390+ & mut server_framed,
391+ destination. host ( ) . to_owned ( ) ,
392+ server_public_key,
393+ server_security_protocol,
394+ & credential_mapping. target ,
395+ ) ;
396+
397+ let ( client_res, server_res) = tokio:: join!( client_credssp_fut, server_credssp_fut) ;
398+ client_res. context ( "CredSSP with client failed" ) ?;
399+ server_res. context ( "CredSSP with server failed" ) ?;
400+
401+ info ! ( "CredSSP MITM completed successfully" ) ;
402+
403+ // Extract streams and any leftover bytes
404+ let ( mut client_stream, client_leftover) = client_framed. into_inner ( ) ;
405+ let ( mut server_stream, server_leftover) = server_framed. into_inner ( ) ;
406+
407+ // Forward any leftover bytes
408+ if !server_leftover. is_empty ( ) {
409+ client_stream
410+ . write_all ( & server_leftover)
411+ . await
412+ . context ( "write server leftover to client" ) ?;
413+ }
414+ if !client_leftover. is_empty ( ) {
415+ server_stream
416+ . write_all ( & client_leftover)
417+ . await
418+ . context ( "write client leftover to server" ) ?;
419+ }
420+
421+ info ! ( "RDP-TLS forwarding (credential injection)" ) ;
422+
423+ // Build SessionInfo for forwarding
424+ let session_info = SessionInfo :: builder ( )
425+ . id ( claims. jet_aid )
426+ . application_protocol ( claims. jet_ap )
427+ . details ( ConnectionModeDetails :: Fwd {
428+ destination_host : destination. clone ( ) ,
429+ } )
430+ . time_to_live ( claims. jet_ttl )
431+ . recording_policy ( claims. jet_rec )
432+ . filtering_policy ( claims. jet_flt )
433+ . build ( ) ;
434+
435+ let disconnect_interest = DisconnectInterest :: from_reconnection_policy ( claims. jet_reuse ) ;
436+
437+ // Plain forwarding for now
438+ Proxy :: builder ( )
439+ . conf ( conf)
440+ . session_info ( session_info)
441+ . address_a ( client_addr)
442+ . transport_a ( client_stream)
443+ . address_b ( server_addr)
444+ . transport_b ( server_stream)
445+ . sessions ( sessions)
446+ . subscriber_tx ( subscriber_tx)
447+ . disconnect_interest ( disconnect_interest)
448+ . build ( )
449+ . select_dissector_and_forward ( )
450+ . await
451+ . context ( "proxy failed" )
452+ }
453+
275454#[ allow( clippy:: too_many_arguments) ]
276455#[ instrument( "fwd" , skip_all, fields( session_id = field:: Empty , target = field:: Empty ) ) ]
277456pub async fn handle (
278- mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send ,
457+ mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send + Sync + ' static ,
279458 client_addr : SocketAddr ,
280459 conf : Arc < Conf > ,
281460 token_cache : & TokenCache ,
282461 jrl : & CurrentJrl ,
283462 sessions : SessionMessageSender ,
284463 subscriber_tx : SubscriberSender ,
285464 active_recordings : & ActiveRecordings ,
465+ credential_store : & CredentialStoreHandle ,
286466) -> anyhow:: Result < ( ) > {
287467 // Special handshake of our RDP extension
288468
289469 trace ! ( "Reading RDCleanPath" ) ;
290470
291471 let cleanpath_pdu = read_cleanpath_pdu ( & mut client_stream)
292472 . await
293- . context ( "couldn’t read clean cleanpath PDU" ) ?;
473+ . context ( "couldn't read clean cleanpath PDU" ) ?;
474+
475+ // Early credential detection: check if we should use RdpProxy instead
476+ let token = cleanpath_pdu
477+ . proxy_auth
478+ . as_deref ( )
479+ . ok_or_else ( || anyhow:: anyhow!( "missing token in RDCleanPath PDU" ) ) ?;
480+
481+ if let Ok ( token_id) = crate :: token:: extract_jti ( token) {
482+ if let Some ( entry) = credential_store. get ( token_id) {
483+ if entry. mapping . is_some ( ) {
484+ // Credentials found! Switch to RdpProxy for credential injection
485+ info ! ( "Switching to RdpProxy for credential injection (WebSocket)" ) ;
486+
487+ return handle_with_credential_injection (
488+ client_stream,
489+ client_addr,
490+ conf,
491+ token_cache,
492+ jrl,
493+ sessions,
494+ subscriber_tx,
495+ active_recordings,
496+ cleanpath_pdu,
497+ entry,
498+ )
499+ . await ;
500+ }
501+ }
502+ }
294503
295504 trace ! ( "Processing RDCleanPath" ) ;
296505
@@ -308,6 +517,7 @@ pub async fn handle(
308517 jrl,
309518 active_recordings,
310519 & sessions,
520+ credential_store,
311521 )
312522 . await
313523 {
0 commit comments