From 434cfa318891df437435609447906b60dc4f7e6c Mon Sep 17 00:00:00 2001 From: Rui Carmo Date: Sun, 5 Jul 2026 07:12:09 +0000 Subject: [PATCH] Add post-auth connection binder hook --- crates/ironrdp-server/src/builder.rs | 14 ++++++- crates/ironrdp-server/src/lib.rs | 6 +-- crates/ironrdp-server/src/server.rs | 55 +++++++++++++++++++++++++++- docs/wrdp/auth-delegation.md | 14 +++++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 docs/wrdp/auth-delegation.md diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs index 0795e6001f..3779f36463 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -11,7 +11,9 @@ use super::display::{DesktopSize, RdpServerDisplay}; #[cfg(feature = "egfx")] use super::gfx::GfxServerFactory; use super::handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler}; -use super::server::{ConnectionHandler, CredentialValidator, RdpServer, RdpServerOptions, RdpServerSecurity}; +use super::server::{ + ConnectionBinder, ConnectionHandler, CredentialValidator, RdpServer, RdpServerOptions, RdpServerSecurity, +}; use crate::{DisplayUpdate, RdpServerDisplayUpdates, SoundServerFactory}; pub struct WantsAddr {} @@ -38,6 +40,7 @@ pub struct BuilderDone { sound_factory: Option>, connection_handler: Option>, credential_validator: Option>, + connection_binder: Option>, #[cfg(feature = "egfx")] gfx_factory: Option>, display_suppressed: Option>, @@ -137,6 +140,7 @@ impl RdpServerBuilder { cliprdr_factory: None, connection_handler: None, credential_validator: None, + connection_binder: None, codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"), max_request_size: RdpServerOptions::DEFAULT_MAX_REQUEST_SIZE, #[cfg(feature = "egfx")] @@ -159,6 +163,7 @@ impl RdpServerBuilder { cliprdr_factory: None, connection_handler: None, credential_validator: None, + connection_binder: None, codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"), max_request_size: RdpServerOptions::DEFAULT_MAX_REQUEST_SIZE, #[cfg(feature = "egfx")] @@ -278,6 +283,12 @@ impl RdpServerBuilder { self } + /// Set a binder that replaces display/input handlers after credentials are accepted. + pub fn with_connection_binder(mut self, binder: Option>) -> Self { + self.state.connection_binder = binder; + self + } + /// Inject a shared NetworkAutoDetect RTT handle (milliseconds, `u32::MAX` /// until the first measurement). The server writes the latest measured RTT /// to the same instance the backend reads. When not called, the server @@ -309,6 +320,7 @@ impl RdpServerBuilder { self.state.autodetect_rtt, ); server.set_credential_validator(self.state.credential_validator); + server.set_connection_binder(self.state.connection_binder); server } } diff --git a/crates/ironrdp-server/src/lib.rs b/crates/ironrdp-server/src/lib.rs index 505d07a2f7..fcebab562e 100644 --- a/crates/ironrdp-server/src/lib.rs +++ b/crates/ironrdp-server/src/lib.rs @@ -33,9 +33,9 @@ pub use handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler}; #[cfg(feature = "helper")] pub use helper::TlsIdentityCtx; pub use server::{ - ConnectionHandler, CredentialDecision, CredentialValidationError, CredentialValidator, Credentials, - ExactMatchCredentialValidator, PostConnectionAction, RdpServer, RdpServerOptions, RdpServerSecurity, ServerEvent, - ServerEventSender, TransportTls, + BoundConnection, ConnectionBinder, ConnectionHandler, CredentialDecision, CredentialValidationError, + CredentialValidator, Credentials, ExactMatchCredentialValidator, PostConnectionAction, RdpServer, RdpServerOptions, + RdpServerSecurity, ServerEvent, ServerEventSender, TransportTls, }; pub use sound::{RdpsndServerHandler, RdpsndServerMessage, SoundServerFactory}; diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index acdfe99dce..d5be484b5b 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -190,6 +190,28 @@ pub trait CredentialValidator: Send + Sync { async fn validate(&self, credentials: &Credentials) -> Result; } +/// Display/input objects bound after a credential validator accepts a client. +/// +/// Servers with per-user desktop/session isolation can start with placeholder +/// display and input handlers, validate the client's credentials, and then +/// replace those placeholders with handlers attached to the authenticated +/// user's session before the RDP client loop starts. +pub struct BoundConnection { + pub display: Box, + pub input: Box, +} + +/// Async post-auth connection binder. +/// +/// This hook runs after [`CredentialValidator`] accepts credentials and before +/// static channels, display updates, or input dispatch begin. It lets a server +/// bind display/input resources to the authenticated identity without creating +/// per-user resources before authentication. +#[async_trait::async_trait] +pub trait ConnectionBinder: Send + Sync { + async fn bind_connection(&self, credentials: &Credentials) -> anyhow::Result; +} + /// A built-in [`CredentialValidator`] that accepts exactly one fixed set of credentials. /// /// This is the validation-policy equivalent of the acceptor's pre-loaded @@ -448,6 +470,7 @@ pub struct RdpServer { ev_receiver: Arc>>, creds: Option, credential_validator: Option>, + connection_binder: Option>, local_addr: Option, autodetect: Option, connection_handler: Option>, @@ -546,6 +569,7 @@ impl RdpServer { ev_receiver: Arc::new(Mutex::new(ev_receiver)), creds: None, credential_validator: None, + connection_binder: None, local_addr: None, autodetect: None, connection_handler, @@ -582,6 +606,15 @@ impl RdpServer { self.credential_validator = validator; } + /// Set or clear a post-auth connection binder. + /// + /// When set, the binder is called after credentials have been validated. + /// The returned display/input handlers replace the server defaults for the + /// accepted connection. + pub fn set_connection_binder(&mut self, binder: Option>) { + self.connection_binder = binder; + } + pub fn event_sender(&self) -> &mpsc::UnboundedSender { &self.ev_sender } @@ -1339,11 +1372,12 @@ impl RdpServer { // async server layer, rather than in the sans-I/O acceptor, because real validators // (PAM/LDAP/DB) are I/O-bound. On rejection, deny with a ServerSetErrorInfoPdu before // closing, matching the acceptor's exact-match denial path. - if let Some(validator) = self.credential_validator.clone() { + let authenticated_credentials = if let Some(validator) = self.credential_validator.clone() { if let Some(creds) = &result.credentials { match validator.validate(creds).await { Ok(CredentialDecision::Accept) => { debug!("Credential validation accepted"); + Some(creds.clone()) } Ok(CredentialDecision::Reject) => { warn!("Credential validation rejected"); @@ -1358,7 +1392,26 @@ impl RdpServer { } } else { debug!("Skipping credential validation (no credentials in AcceptorResult)"); + None } + } else { + result.credentials.clone() + }; + + if let Some(binder) = self.connection_binder.clone() { + let Some(credentials) = authenticated_credentials.as_ref() else { + warn!("Connection binder configured but no authenticated credentials are available"); + send_access_denied(result.io_channel_id, result.user_channel_id, writer).await?; + bail!("no authenticated credentials for connection binding"); + }; + + let bound = binder + .bind_connection(credentials) + .await + .context("connection binder failed")?; + *self.display.lock().await = bound.display; + *self.handler.lock().await = bound.input; + debug!("Connection binder installed display/input handlers"); } if !result.input_events.is_empty() { diff --git a/docs/wrdp/auth-delegation.md b/docs/wrdp/auth-delegation.md new file mode 100644 index 0000000000..1f7f1cfb6b --- /dev/null +++ b/docs/wrdp/auth-delegation.md @@ -0,0 +1,14 @@ +# Post-auth connection binding for multi-user servers + +`wrdp` follows the same multi-user architecture model as `xrdp-sesman`: a +single public RDP listener authenticates the client first, then delegates the +connection to a per-user desktop/session stack. + +That model needs a server hook that runs after credentials have been accepted +but before display updates and input dispatch begin. The hook lets a server +start or locate the authenticated user's session and then replace placeholder +handlers with display/input handlers bound to that session. + +The `ConnectionBinder` API keeps protocol ownership inside IronRDP while +allowing downstream servers to keep user/session lifecycle code outside the RDP +state machine.