This guide will walk you through creating a simple RDP client that connects to a server, captures graphics updates, and saves them as an image.
Prerequisites
Ensure you have:
Rust 1.88.0 or later installed
An RDP server to connect to (Windows machine, RDP server, etc.)
Basic familiarity with Rust and async programming
Project Setup
Create a new Rust project
cargo new rdp-screenshot
cd rdp-screenshot
Add dependencies
Edit Cargo.toml to include IronRDP and required dependencies: [ dependencies ]
ironrdp = { version = "0.14" , features = [ "connector" , "session" , "graphics" ] }
ironrdp-blocking = "0.8"
ironrdp-pdu = "0.7"
tokio-rustls = "0.26"
sspi = { version = "0.18" , features = [ "network_client" ] }
image = { version = "0.25" , default-features = false , features = [ "png" ] }
x509-cert = { version = "0.2" , default-features = false , features = [ "std" ] }
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3" , features = [ "env-filter" ] }
Set up logging (optional but recommended)
use tracing_subscriber :: { prelude ::* , EnvFilter };
use tracing :: metadata :: LevelFilter ;
fn setup_logging () -> anyhow :: Result <()> {
let fmt_layer = tracing_subscriber :: fmt :: layer () . compact ();
let env_filter = EnvFilter :: builder ()
. with_default_directive ( LevelFilter :: WARN . into ())
. with_env_var ( "IRONRDP_LOG" )
. from_env_lossy ();
tracing_subscriber :: registry ()
. with ( fmt_layer )
. with ( env_filter )
. try_init () ? ;
Ok (())
}
Run with logging: IRONRDP_LOG = info cargo run
Building a Simple Screenshot Client
Create a configuration for connecting to the RDP server:
use ironrdp :: connector :: { self , Credentials , DesktopSize };
use ironrdp :: pdu :: gcc :: KeyboardType ;
use ironrdp :: pdu :: rdp :: capability_sets :: MajorPlatformType ;
use ironrdp_pdu :: rdp :: client_info :: { CompressionType , PerformanceFlags , TimezoneInfo };
fn build_config (
username : String ,
password : String ,
domain : Option < String >,
) -> anyhow :: Result < connector :: Config > {
Ok ( connector :: Config {
credentials : Credentials :: UsernamePassword { username , password },
domain ,
enable_tls : false ,
enable_credssp : true ,
keyboard_type : KeyboardType :: IbmEnhanced ,
keyboard_subtype : 0 ,
keyboard_layout : 0 ,
keyboard_functional_keys_count : 12 ,
ime_file_name : String :: new (),
dig_product_id : String :: new (),
desktop_size : DesktopSize {
width : 1280 ,
height : 1024 ,
},
bitmap : None ,
client_build : 0 ,
client_name : "ironrdp-quickstart" . to_owned (),
client_dir : "C: \\ Windows \\ System32 \\ mstscax.dll" . to_owned (),
platform : MajorPlatformType :: UNIX ,
enable_server_pointer : false ,
request_data : None ,
autologon : false ,
enable_audio_playback : false ,
compression_type : Some ( CompressionType :: Rdp61 ),
pointer_software_rendering : true ,
multitransport_flags : None ,
performance_flags : PerformanceFlags :: default (),
desktop_scale_factor : 0 ,
hardware_id : None ,
license_cache : None ,
timezone_info : TimezoneInfo :: default (),
alternate_shell : String :: new (),
work_dir : String :: new (),
})
}
The configuration includes many fields for fine-grained control. Most can use sensible defaults as shown above.
Step 2: Establish the Connection
Connect to the RDP server using blocking I/O:
use std :: net :: TcpStream ;
use std :: time :: Duration ;
use ironrdp :: connector :: { ClientConnector , ConnectionResult };
use ironrdp_blocking :: Framed ;
use sspi :: network_client :: reqwest_network_client :: ReqwestNetworkClient ;
use tokio_rustls :: rustls;
use anyhow :: Context ;
type UpgradedFramed = Framed < rustls :: StreamOwned < rustls :: ClientConnection , TcpStream >>;
fn connect (
config : connector :: Config ,
server_name : String ,
port : u16 ,
) -> anyhow :: Result <( ConnectionResult , UpgradedFramed )> {
// Resolve server address
let server_addr = ( server_name . as_str (), port )
. to_socket_addrs () ?
. next ()
. context ( "failed to resolve server address" ) ? ;
// Establish TCP connection
let tcp_stream = TcpStream :: connect ( server_addr )
. context ( "failed to connect to server" ) ? ;
tcp_stream
. set_read_timeout ( Some ( Duration :: from_secs ( 3 )))
. expect ( "set_read_timeout failed" );
let client_addr = tcp_stream . local_addr () ? ;
let mut framed = Framed :: new ( tcp_stream );
let mut connector = ClientConnector :: new ( config , client_addr );
// Begin connection sequence
let should_upgrade = ironrdp_blocking :: connect_begin ( & mut framed , & mut connector )
. context ( "connection begin failed" ) ? ;
// Perform TLS upgrade
let initial_stream = framed . into_inner_no_leftover ();
let ( upgraded_stream , server_public_key ) =
tls_upgrade ( initial_stream , server_name . clone ()) ? ;
let upgraded = ironrdp_blocking :: mark_as_upgraded ( should_upgrade , & mut connector );
let mut upgraded_framed = Framed :: new ( upgraded_stream );
// Finalize connection with authentication
let mut network_client = ReqwestNetworkClient ;
let connection_result = ironrdp_blocking :: connect_finalize (
upgraded ,
connector ,
& mut upgraded_framed ,
& mut network_client ,
server_name . into (),
server_public_key ,
None ,
) ? ;
Ok (( connection_result , upgraded_framed ))
}
Step 3: TLS Upgrade Helper
Implement TLS upgrade with certificate verification disabled (for testing):
fn tls_upgrade (
stream : TcpStream ,
server_name : String ,
) -> anyhow :: Result <( rustls :: StreamOwned < rustls :: ClientConnection , TcpStream >, Vec < u8 >)> {
use tokio_rustls :: rustls :: client :: danger :: { HandshakeSignatureValid , ServerCertVerified };
#[derive( Debug )]
struct NoCertificateVerification ;
impl tokio_rustls :: rustls :: client :: danger :: ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert (
& self ,
_ : & tokio_rustls :: rustls :: pki_types :: CertificateDer <' _ >,
_ : & [ tokio_rustls :: rustls :: pki_types :: CertificateDer <' _ >],
_ : & tokio_rustls :: rustls :: pki_types :: ServerName <' _ >,
_ : & [ u8 ],
_ : tokio_rustls :: rustls :: pki_types :: UnixTime ,
) -> Result < ServerCertVerified , tokio_rustls :: rustls :: Error > {
Ok ( ServerCertVerified :: assertion ())
}
fn verify_tls12_signature (
& self ,
_ : & [ u8 ],
_ : & tokio_rustls :: rustls :: pki_types :: CertificateDer <' _ >,
_ : & tokio_rustls :: rustls :: DigitallySignedStruct ,
) -> Result < HandshakeSignatureValid , tokio_rustls :: rustls :: Error > {
Ok ( HandshakeSignatureValid :: assertion ())
}
fn verify_tls13_signature (
& self ,
_ : & [ u8 ],
_ : & tokio_rustls :: rustls :: pki_types :: CertificateDer <' _ >,
_ : & tokio_rustls :: rustls :: DigitallySignedStruct ,
) -> Result < HandshakeSignatureValid , tokio_rustls :: rustls :: Error > {
Ok ( HandshakeSignatureValid :: assertion ())
}
fn supported_verify_schemes ( & self ) -> Vec < tokio_rustls :: rustls :: SignatureScheme > {
use tokio_rustls :: rustls :: SignatureScheme ::* ;
vec! [
RSA_PKCS1_SHA256 , ECDSA_NISTP256_SHA256 ,
RSA_PSS_SHA256 , ED25519 ,
]
}
}
let mut config = rustls :: client :: ClientConfig :: builder ()
. dangerous ()
. with_custom_certificate_verifier ( std :: sync :: Arc :: new ( NoCertificateVerification ))
. with_no_client_auth ();
config . resumption = rustls :: client :: Resumption :: disabled ();
let config = std :: sync :: Arc :: new ( config );
let server_name = server_name . try_into () ? ;
let client = rustls :: ClientConnection :: new ( config , server_name ) ? ;
let mut tls_stream = rustls :: StreamOwned :: new ( client , stream );
use std :: io :: Write ;
tls_stream . flush () ? ;
let cert = tls_stream
. conn
. peer_certificates ()
. and_then ( | certs | certs . first ())
. context ( "peer certificate missing" ) ? ;
use x509_cert :: der :: Decode ;
let cert = x509_cert :: Certificate :: from_der ( cert ) ? ;
let server_public_key = cert
. tbs_certificate
. subject_public_key_info
. subject_public_key
. as_bytes ()
. context ( "subject public key not aligned" ) ?
. to_owned ();
Ok (( tls_stream , server_public_key ))
}
This example disables certificate verification for simplicity. In production, always validate server certificates properly.
Step 4: Process the Active Stage
Handle the active session and capture graphics updates:
use ironrdp :: session :: { ActiveStage , ActiveStageOutput };
use ironrdp :: session :: image :: DecodedImage ;
use std :: io :: Write ;
fn active_stage (
connection_result : ConnectionResult ,
mut framed : UpgradedFramed ,
image : & mut DecodedImage ,
) -> anyhow :: Result <()> {
let mut active_stage = ActiveStage :: new ( connection_result );
loop {
let ( action , payload ) = match framed . read_pdu () {
Ok ( frame ) => frame ,
Err ( e ) if e . kind () == std :: io :: ErrorKind :: WouldBlock => break ,
Err ( e ) => return Err ( e . into ()),
};
let outputs = active_stage . process ( image , action , & payload ) ? ;
for output in outputs {
match output {
ActiveStageOutput :: ResponseFrame ( frame ) => {
framed . write_all ( & frame ) ? ;
}
ActiveStageOutput :: Terminate ( _ ) => return Ok (()),
_ => {}
}
}
}
Ok (())
}
Step 5: Complete Example
use std :: net :: { TcpStream , ToSocketAddrs };
use std :: path :: PathBuf ;
use std :: time :: Duration ;
use anyhow :: Context ;
use ironrdp :: connector :: { self , Credentials , ConnectionResult };
use ironrdp :: pdu :: gcc :: KeyboardType ;
use ironrdp :: pdu :: rdp :: capability_sets :: MajorPlatformType ;
use ironrdp :: session :: { ActiveStage , ActiveStageOutput };
use ironrdp :: session :: image :: DecodedImage ;
use ironrdp_blocking :: Framed ;
use ironrdp_pdu :: rdp :: client_info :: { CompressionType , PerformanceFlags , TimezoneInfo };
use sspi :: network_client :: reqwest_network_client :: ReqwestNetworkClient ;
use tokio_rustls :: rustls;
fn main () -> anyhow :: Result <()> {
let config = build_config (
"username" . to_owned (),
"password" . to_owned (),
None ,
) ? ;
let ( connection_result , framed ) = connect (
config ,
"192.168.1.100" . to_owned (),
3389 ,
) ? ;
let mut image = DecodedImage :: new (
ironrdp_graphics :: image_processing :: PixelFormat :: RgbA32 ,
connection_result . desktop_size . width,
connection_result . desktop_size . height,
);
active_stage ( connection_result , framed , & mut image ) ? ;
// Save image
let img : image :: ImageBuffer < image :: Rgba < u8 >, _ > =
image :: ImageBuffer :: from_raw (
u32 :: from ( image . width ()),
u32 :: from ( image . height ()),
image . data (),
)
. context ( "invalid image" ) ? ;
img . save ( "output.png" ) ? ;
println! ( "Saved screenshot to output.png" );
Ok (())
}
// Include build_config, connect, tls_upgrade, and active_stage from above
Running the Example
Run with your RDP server details
./target/release/rdp-screenshot
Or with logging: IRONRDP_LOG = info ./target/release/rdp-screenshot
Check the output
You should see output.png in your project directory containing a screenshot of the remote desktop.
Key Concepts Explained
Connection Lifecycle
Initial Connection
connect_begin() starts the RDP handshake over TCP
TLS Upgrade
The connection is upgraded to TLS for security
Authentication
connect_finalize() completes authentication (CredSSP, NLA, etc.)
Active Stage
The session enters active mode, processing graphics updates and user input
Understanding ActiveStage
The ActiveStage is the heart of an RDP session:
pub struct ActiveStage { /* ... */ }
impl ActiveStage {
pub fn process (
& mut self ,
image : & mut DecodedImage ,
action : Action ,
payload : & [ u8 ],
) -> Result < Vec < ActiveStageOutput >> {
// Processes incoming PDUs and updates the image buffer
}
}
Key outputs:
ActiveStageOutput::ResponseFrame - Data to send back to the server
ActiveStageOutput::GraphicsUpdate - Screen content changed
ActiveStageOutput::Terminate - Connection closed
Image Decoding
DecodedImage manages the remote desktop framebuffer:
use ironrdp_graphics :: image_processing :: PixelFormat ;
let mut image = DecodedImage :: new (
PixelFormat :: RgbA32 , // 32-bit RGBA format
1920 , // Width
1080 , // Height
);
// After processing updates, access the raw pixel data:
let pixel_data : & [ u8 ] = image . data ();
Building an Async Client
For production use, prefer async I/O with Tokio:
[ dependencies ]
ironrdp = { version = "0.14" , features = [ "connector" , "session" ] }
ironrdp-tokio = "0.5"
tokio = { version = "1" , features = [ "full" ] }
use ironrdp_tokio :: TokioFramed ;
use tokio :: net :: TcpStream ;
#[tokio :: main]
async fn main () -> anyhow :: Result <()> {
let stream = TcpStream :: connect ( "192.168.1.100:3389" ) . await ? ;
let framed = TokioFramed :: new ( stream );
// Use async versions of connect_begin/finalize
// ...
Ok (())
}
Refer to the full ironrdp-client implementation in the repository for a complete async example with GUI support.
Common Issues
Symptom: TcpStream::connect hangs or times outSolutions:
Verify the server is reachable (ping the host)
Check firewall rules (port 3389 must be open)
Ensure RDP is enabled on the target machine
Symptom: Error during connect_finalizeSolutions:
Verify username and password are correct
Check domain name (use None for local accounts)
Ensure CredSSP is enabled on the server
Black or incomplete image
Symptom: Screenshot is saved but appears blankSolutions:
Increase the read timeout (some servers send updates slowly)
Add a delay before saving the image to capture more updates
Check logs for errors during graphics decoding
Symptom: Error during tls_upgradeSolutions:
Ensure the server supports TLS 1.2 or later
Try with enable_tls: false for testing (not recommended for production)
Check server certificate validity
Next Steps
Now that you’ve built a basic client, explore:
Building Servers Create custom RDP servers with IronRDP
Virtual Channels Implement clipboard, audio, and custom channels
Examples Study the full screenshot and server examples
API Documentation Deep dive into the complete API reference
Real-World Examples
The IronRDP repository includes production-ready examples:
Screenshot Example
Located at crates/ironrdp/examples/screenshot.rs - a complete blocking client in ~500 lines:
cargo run --example=screenshot -- \
--host 192.168.1.100 \
-u Administrator \
-p MyPassword123 \
-o screenshot.png \
--compression-enabled true \
--compression-level 3
Server Example
Located at crates/ironrdp/examples/server.rs - a minimal RDP server:
cargo run --example=server -- \
--bind-addr 0.0.0.0:3389 \
--user testuser \
--pass testpass \
--sec hybrid
Study these examples for production patterns, error handling, and best practices.