Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion crates/ironrdp-server/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -38,6 +40,7 @@ pub struct BuilderDone {
sound_factory: Option<Box<dyn SoundServerFactory>>,
connection_handler: Option<Box<dyn ConnectionHandler>>,
credential_validator: Option<Arc<dyn CredentialValidator>>,
connection_binder: Option<Arc<dyn ConnectionBinder>>,
#[cfg(feature = "egfx")]
gfx_factory: Option<Box<dyn GfxServerFactory>>,
display_suppressed: Option<Arc<AtomicBool>>,
Expand Down Expand Up @@ -137,6 +140,7 @@ impl RdpServerBuilder<WantsDisplay> {
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")]
Expand All @@ -159,6 +163,7 @@ impl RdpServerBuilder<WantsDisplay> {
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")]
Expand Down Expand Up @@ -278,6 +283,12 @@ impl RdpServerBuilder<BuilderDone> {
self
}

/// Set a binder that replaces display/input handlers after credentials are accepted.
pub fn with_connection_binder(mut self, binder: Option<Arc<dyn ConnectionBinder>>) -> 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
Expand Down Expand Up @@ -309,6 +320,7 @@ impl RdpServerBuilder<BuilderDone> {
self.state.autodetect_rtt,
);
server.set_credential_validator(self.state.credential_validator);
server.set_connection_binder(self.state.connection_binder);
server
}
}
Expand Down
6 changes: 3 additions & 3 deletions crates/ironrdp-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
55 changes: 54 additions & 1 deletion crates/ironrdp-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,28 @@ pub trait CredentialValidator: Send + Sync {
async fn validate(&self, credentials: &Credentials) -> Result<CredentialDecision, CredentialValidationError>;
}

/// 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<dyn RdpServerDisplay>,
pub input: Box<dyn RdpServerInputHandler>,
}

/// 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<BoundConnection>;
}

/// A built-in [`CredentialValidator`] that accepts exactly one fixed set of credentials.
///
/// This is the validation-policy equivalent of the acceptor's pre-loaded
Expand Down Expand Up @@ -448,6 +470,7 @@ pub struct RdpServer {
ev_receiver: Arc<Mutex<mpsc::UnboundedReceiver<ServerEvent>>>,
creds: Option<Credentials>,
credential_validator: Option<Arc<dyn CredentialValidator>>,
connection_binder: Option<Arc<dyn ConnectionBinder>>,
local_addr: Option<SocketAddr>,
autodetect: Option<AutoDetectManager>,
connection_handler: Option<Box<dyn ConnectionHandler>>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Arc<dyn ConnectionBinder>>) {
self.connection_binder = binder;
}

pub fn event_sender(&self) -> &mpsc::UnboundedSender<ServerEvent> {
&self.ev_sender
}
Expand Down Expand Up @@ -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");
Expand All @@ -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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed on the canonical rcarmo/IronRDP:wrdp branch in 3b05f6fc70d91c2529df8454ce73a292c05195e6: the connection binder is now guarded with !result.reactivation, so it runs only for the initial accept on a TCP connection. Reactivation now skips rebinding and preserves the installed display/input handlers.

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() {
Expand Down
14 changes: 14 additions & 0 deletions docs/wrdp/auth-delegation.md
Original file line number Diff line number Diff line change
@@ -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.