ironrdp-server for building custom RDP servers. This guide demonstrates how to create a basic server that accepts connections, handles authentication, and sends display updates.
Server Overview
Theironrdp-server crate provides a high-level async API built on Tokio. A minimal server requires:
- Input handler - Process keyboard and mouse events from clients
- Display handler - Provide desktop size and frame updates
- Security configuration - TLS, NLA, or no security
- Optional channel handlers - Clipboard, audio, etc.
Basic Server Example
Here’s a complete working server (based onexamples/server.rs):
use ironrdp::server::{RdpServerInputHandler, KeyboardEvent, MouseEvent};
#[derive(Clone, Debug)]
struct Handler;
impl RdpServerInputHandler for Handler {
fn keyboard(&mut self, event: KeyboardEvent) {
println!("Keyboard event: {:?}", event);
}
fn mouse(&mut self, event: MouseEvent) {
println!("Mouse event: {:?}", event);
}
}
use ironrdp::server::{RdpServerDisplay, RdpServerDisplayUpdates};
use ironrdp::server::{DisplayUpdate, BitmapUpdate, PixelFormat};
use ironrdp::connector::DesktopSize;
use core::num::{NonZeroU16, NonZeroUsize};
#[async_trait::async_trait]
impl RdpServerDisplay for Handler {
async fn size(&mut self) -> DesktopSize {
DesktopSize {
width: 1920,
height: 1080,
}
}
async fn updates(&mut self) -> anyhow::Result<Box<dyn RdpServerDisplayUpdates>> {
Ok(Box::new(DisplayUpdates {}))
}
}
struct DisplayUpdates;
#[async_trait::async_trait]
impl RdpServerDisplayUpdates for DisplayUpdates {
async fn next_update(&mut self) -> anyhow::Result<Option<DisplayUpdate>> {
// Generate random colored rectangle as demo
tokio::time::sleep(Duration::from_millis(100)).await;
let x: u16 = 100;
let y: u16 = 100;
let width = NonZeroU16::new(200).unwrap();
let height = NonZeroU16::new(150).unwrap();
// Create RGBA bitmap data (200x150x4 bytes)
let mut data = vec![0u8; 200 * 150 * 4];
for i in 0..(200 * 150) {
data[i * 4] = 255; // Blue
data[i * 4 + 1] = 0; // Green
data[i * 4 + 2] = 0; // Red
data[i * 4 + 3] = 255; // Alpha
}
let bitmap = BitmapUpdate {
x,
y,
width,
height,
format: PixelFormat::BgrA32,
data: data.into(),
stride: NonZeroUsize::new(200 * 4).unwrap(),
};
Ok(Some(DisplayUpdate::Bitmap(bitmap)))
}
}
use ironrdp::server::{RdpServer, TlsIdentityCtx, Credentials};
use std::net::SocketAddr;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let bind_addr: SocketAddr = "127.0.0.1:3389".parse()?;
let handler = Handler;
let server_builder = RdpServer::builder()
.with_addr(bind_addr);
// Configure security mode
let server_builder = if let Some((cert_path, key_path)) = get_tls_paths() {
let identity = TlsIdentityCtx::init_from_paths(cert_path, key_path)?;
let acceptor = identity.make_acceptor()?;
// Use hybrid mode (TLS + NLA)
server_builder.with_hybrid(acceptor, identity.pub_key)
} else {
// No security (for testing only)
server_builder.with_no_security()
};
let mut server = server_builder
.with_input_handler(handler.clone())
.with_display_handler(handler)
.build();
// Set required credentials
server.set_credentials(Some(Credentials {
username: "user".to_owned(),
password: "pass".to_owned(),
domain: None,
}));
println!("Server listening on {}", bind_addr);
server.run().await
}
Security Modes
No Security (Testing Only)
TLS Only
Hybrid (TLS + NLA/CredSSP)
Most secure, recommended for production:Virtual Channel Support
Adding Clipboard Support
Adding Audio Output (RDPSND)
Advanced Display Updates
Efficient Frame Generation
For real-world servers, generate updates based on actual screen content:Credentials and Authentication
Set allowed credentials before running:Running in Production
Generate TLS Certificate
Environment Configuration
Logging
Next Steps
- Virtual Channels - Implement custom channels
- Graphics Rendering - Optimize display updates
- Async I/O - Understand the async architecture

