@@ -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 ;
@@ -164,6 +166,7 @@ struct CleanPathResult {
164166 x224_rsp : Vec < u8 > ,
165167}
166168
169+ #[ allow( clippy:: too_many_arguments) ]
167170async fn process_cleanpath (
168171 cleanpath_pdu : RDCleanPathPdu ,
169172 client_addr : SocketAddr ,
@@ -172,6 +175,7 @@ async fn process_cleanpath(
172175 jrl : & CurrentJrl ,
173176 active_recordings : & ActiveRecordings ,
174177 sessions : & SessionMessageSender ,
178+ _credential_store : & CredentialStoreHandle ,
175179) -> Result < CleanPathResult , CleanPathError > {
176180 use crate :: utils;
177181
@@ -272,25 +276,231 @@ async fn process_cleanpath(
272276 } )
273277}
274278
279+ /// Handle RDP connection with credential injection via CredSSP MITM
280+ #[ allow( clippy:: too_many_arguments) ]
281+ async fn handle_with_credential_injection (
282+ mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send + Sync + ' static ,
283+ client_addr : SocketAddr ,
284+ conf : Arc < Conf > ,
285+ token_cache : & TokenCache ,
286+ jrl : & CurrentJrl ,
287+ sessions : SessionMessageSender ,
288+ subscriber_tx : SubscriberSender ,
289+ active_recordings : & ActiveRecordings ,
290+ cleanpath_pdu : RDCleanPathPdu ,
291+ _credential_entry : crate :: credential:: ArcCredentialEntry ,
292+ ) -> anyhow:: Result < ( ) > {
293+ let token = cleanpath_pdu. proxy_auth . as_ref ( ) . context ( "missing token" ) ?;
294+
295+ // Authorize the token
296+ let claims = authorize ( client_addr, token, & conf, token_cache, jrl, active_recordings, None )
297+ . map_err ( |e| anyhow:: anyhow!( "authorization failed: {}" , e) ) ?;
298+
299+ let crate :: token:: ConnectionMode :: Fwd { targets : _ } = claims. jet_cm else {
300+ anyhow:: bail!( "unexpected connection mode" ) ;
301+ } ;
302+
303+ let span = tracing:: Span :: current ( ) ;
304+ span. record ( "session_id" , claims. jet_aid . to_string ( ) ) ;
305+
306+ info ! ( "Credential injection: performing CredSSP MITM" ) ;
307+
308+ // Run normal RDCleanPath flow (this will handle server-side TLS and get certs)
309+ let CleanPathResult {
310+ destination,
311+ server_addr,
312+ server_stream,
313+ x224_rsp,
314+ ..
315+ } = process_cleanpath (
316+ cleanpath_pdu,
317+ client_addr,
318+ & conf,
319+ token_cache,
320+ jrl,
321+ active_recordings,
322+ & sessions,
323+ & CredentialStoreHandle :: new ( ) , // Dummy, not used in process_cleanpath
324+ )
325+ . await
326+ . map_err ( |e| anyhow:: anyhow!( "RDCleanPath processing failed: {}" , e) ) ?;
327+
328+ // Extract server security protocol from X224 response (before x224_rsp is moved)
329+ let x224_confirm: ironrdp_pdu:: x224:: X224 < nego:: ConnectionConfirm > =
330+ ironrdp_core:: decode ( & x224_rsp) . context ( "decode X224 connection confirm" ) ?;
331+ let server_security_protocol = match & x224_confirm. 0 {
332+ nego:: ConnectionConfirm :: Response { protocol, .. } => {
333+ if !protocol. intersects ( nego:: SecurityProtocol :: HYBRID | nego:: SecurityProtocol :: HYBRID_EX ) {
334+ anyhow:: bail!(
335+ "server selected security protocol {protocol}, which is not supported for credential injection"
336+ ) ;
337+ }
338+ * protocol
339+ }
340+ nego:: ConnectionConfirm :: Failure { code } => {
341+ anyhow:: bail!( "RDP session initiation failed with code {code}" ) ;
342+ }
343+ } ;
344+
345+ // Send RDCleanPath response to client (includes server certs)
346+ let x509_chain = server_stream
347+ . get_ref ( )
348+ . 1
349+ . peer_certificates ( )
350+ . context ( "no peer certificate found in TLS transport" ) ?
351+ . iter ( )
352+ . map ( |cert| cert. to_vec ( ) ) ;
353+
354+ trace ! ( "Sending RDCleanPath response" ) ;
355+
356+ let rdcleanpath_rsp = RDCleanPathPdu :: new_response ( server_addr. to_string ( ) , x224_rsp, x509_chain)
357+ . map_err ( |e| anyhow:: anyhow!( "couldn't build RDCleanPath response: {e}" ) ) ?;
358+
359+ send_clean_path_response ( & mut client_stream, & rdcleanpath_rsp) . await ?;
360+
361+ info ! ( "RDCleanPath response sent, now performing CredSSP MITM" ) ;
362+
363+ // Verify TLS is configured
364+ conf. tls . as_ref ( ) . context ( "TLS required for credential injection" ) ?;
365+
366+ // Get credential mapping
367+ let credential_mapping = _credential_entry. mapping . as_ref ( ) . context ( "no credential mapping" ) ?;
368+
369+ // Extract server public key from TLS stream
370+ let server_public_key =
371+ crate :: rdp_proxy:: extract_tls_server_public_key ( & server_stream) . context ( "extract server TLS public key" ) ?;
372+
373+ // Wrap streams in TokioFramed for CredSSP
374+ let mut client_framed = ironrdp_tokio:: TokioFramed :: new ( client_stream) ;
375+ let mut server_framed = ironrdp_tokio:: TokioFramed :: new ( server_stream) ;
376+
377+ // Use HYBRID_EX for client (web clients typically use this)
378+ let client_security_protocol = nego:: SecurityProtocol :: HYBRID_EX ;
379+
380+ // Perform CredSSP MITM (in parallel)
381+ // Note: Client expects server's public key (since we sent server certs in RDCleanPath response)
382+ let client_credssp_fut = crate :: rdp_proxy:: perform_credssp_with_client (
383+ & mut client_framed,
384+ client_addr. ip ( ) ,
385+ server_public_key. clone ( ) ,
386+ client_security_protocol,
387+ & credential_mapping. proxy ,
388+ ) ;
389+
390+ let server_credssp_fut = crate :: rdp_proxy:: perform_credssp_with_server (
391+ & mut server_framed,
392+ destination. host ( ) . to_owned ( ) ,
393+ server_public_key,
394+ server_security_protocol,
395+ & credential_mapping. target ,
396+ ) ;
397+
398+ let ( client_res, server_res) = tokio:: join!( client_credssp_fut, server_credssp_fut) ;
399+ client_res. context ( "CredSSP with client failed" ) ?;
400+ server_res. context ( "CredSSP with server failed" ) ?;
401+
402+ info ! ( "CredSSP MITM completed successfully" ) ;
403+
404+ // Extract streams and any leftover bytes
405+ let ( mut client_stream, client_leftover) = client_framed. into_inner ( ) ;
406+ let ( mut server_stream, server_leftover) = server_framed. into_inner ( ) ;
407+
408+ // Forward any leftover bytes
409+ if !server_leftover. is_empty ( ) {
410+ client_stream
411+ . write_all ( & server_leftover)
412+ . await
413+ . context ( "write server leftover to client" ) ?;
414+ }
415+ if !client_leftover. is_empty ( ) {
416+ server_stream
417+ . write_all ( & client_leftover)
418+ . await
419+ . context ( "write client leftover to server" ) ?;
420+ }
421+
422+ info ! ( "RDP-TLS forwarding (credential injection)" ) ;
423+
424+ // Build SessionInfo for forwarding
425+ let session_info = SessionInfo :: builder ( )
426+ . id ( claims. jet_aid )
427+ . application_protocol ( claims. jet_ap )
428+ . details ( ConnectionModeDetails :: Fwd {
429+ destination_host : destination. clone ( ) ,
430+ } )
431+ . time_to_live ( claims. jet_ttl )
432+ . recording_policy ( claims. jet_rec )
433+ . filtering_policy ( claims. jet_flt )
434+ . build ( ) ;
435+
436+ let disconnect_interest = DisconnectInterest :: from_reconnection_policy ( claims. jet_reuse ) ;
437+
438+ // Plain forwarding for now
439+ Proxy :: builder ( )
440+ . conf ( conf)
441+ . session_info ( session_info)
442+ . address_a ( client_addr)
443+ . transport_a ( client_stream)
444+ . address_b ( server_addr)
445+ . transport_b ( server_stream)
446+ . sessions ( sessions)
447+ . subscriber_tx ( subscriber_tx)
448+ . disconnect_interest ( disconnect_interest)
449+ . build ( )
450+ . select_dissector_and_forward ( )
451+ . await
452+ . context ( "proxy failed" )
453+ }
454+
275455#[ allow( clippy:: too_many_arguments) ]
276456#[ instrument( "fwd" , skip_all, fields( session_id = field:: Empty , target = field:: Empty ) ) ]
277457pub async fn handle (
278- mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send ,
458+ mut client_stream : impl AsyncRead + AsyncWrite + Unpin + Send + Sync + ' static ,
279459 client_addr : SocketAddr ,
280460 conf : Arc < Conf > ,
281461 token_cache : & TokenCache ,
282462 jrl : & CurrentJrl ,
283463 sessions : SessionMessageSender ,
284464 subscriber_tx : SubscriberSender ,
285465 active_recordings : & ActiveRecordings ,
466+ credential_store : & CredentialStoreHandle ,
286467) -> anyhow:: Result < ( ) > {
287468 // Special handshake of our RDP extension
288469
289470 trace ! ( "Reading RDCleanPath" ) ;
290471
291472 let cleanpath_pdu = read_cleanpath_pdu ( & mut client_stream)
292473 . await
293- . context ( "couldn’t read clean cleanpath PDU" ) ?;
474+ . context ( "couldn't read clean cleanpath PDU" ) ?;
475+
476+ // Early credential detection: check if we should use RdpProxy instead
477+ let token = cleanpath_pdu
478+ . proxy_auth
479+ . as_deref ( )
480+ . ok_or_else ( || anyhow:: anyhow!( "missing token in RDCleanPath PDU" ) ) ?;
481+
482+ if let Some ( entry) = crate :: token:: extract_jti ( token)
483+ . ok ( )
484+ . and_then ( |token_id| credential_store. get ( token_id) )
485+ . filter ( |entry| entry. mapping . is_some ( ) )
486+ {
487+ // Credentials found! Switch to RdpProxy for credential injection
488+ info ! ( "Switching to RdpProxy for credential injection (WebSocket)" ) ;
489+
490+ return handle_with_credential_injection (
491+ client_stream,
492+ client_addr,
493+ conf,
494+ token_cache,
495+ jrl,
496+ sessions,
497+ subscriber_tx,
498+ active_recordings,
499+ cleanpath_pdu,
500+ entry,
501+ )
502+ . await ;
503+ }
294504
295505 trace ! ( "Processing RDCleanPath" ) ;
296506
@@ -308,6 +518,7 @@ pub async fn handle(
308518 jrl,
309519 active_recordings,
310520 & sessions,
521+ credential_store,
311522 )
312523 . await
313524 {
@@ -537,3 +748,4 @@ impl From<&io::Error> for WsaError {
537748 }
538749 }
539750}
751+
0 commit comments