Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Devolutions/IronRDP/llms.txt
Use this file to discover all available pages before exploring further.
RDP virtual channels enable extending the protocol with custom functionality. IronRDP supports both static virtual channels (SVC) and dynamic virtual channels (DVC), along with built-in implementations for common channels.
Channel Types
Static Virtual Channels (SVC)
Established during connection setup. Examples:
CLIPRDR - Clipboard redirection
RDPSND - Audio output
RDPDR - Device redirection
DRDYNVC - Dynamic channel transport
Dynamic Virtual Channels (DVC)
Created on-demand over the DRDYNVC static channel. More flexible for custom protocols.
Built-in Channels
Clipboard (CLIPRDR)
The clipboard channel enables copy/paste between client and server.
Client-side Implementation
use ironrdp::cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory};
use ironrdp_cliprdr_native::NativeCliprdrBackend;
struct MyCliprdrFactory;
impl CliprdrBackendFactory for MyCliprdrFactory {
fn build_cliprdr_backend(&self) -> Box<dyn CliprdrBackend> {
// Use native clipboard integration
Box::new(NativeCliprdrBackend::new())
}
}
Server-side Implementation
use ironrdp::server::{CliprdrServerFactory, ServerEventSender, ServerEvent};
use ironrdp::server::tokio::sync::mpsc::UnboundedSender;
use ironrdp_cliprdr_native::StubCliprdrBackend;
struct ServerCliprdrFactory;
impl CliprdrBackendFactory for ServerCliprdrFactory {
fn build_cliprdr_backend(&self) -> Box<dyn CliprdrBackend> {
Box::new(StubCliprdrBackend::new())
}
}
impl ServerEventSender for ServerCliprdrFactory {
fn set_sender(&mut self, _sender: UnboundedSender<ServerEvent>) {}
}
impl CliprdrServerFactory for ServerCliprdrFactory {}
// Add to server builder
let cliprdr = Box::new(ServerCliprdrFactory);
let server = server_builder
.with_cliprdr_factory(Some(cliprdr))
.build();
Custom Clipboard Backend
Implement your own clipboard logic:
use ironrdp::cliprdr::backend::CliprdrBackend;
use ironrdp::cliprdr::pdu::*;
struct MyClipboard {
content: Vec<u8>,
}
impl CliprdrBackend for MyClipboard {
fn capabilities(&self) -> anyhow::Result<ClipboardCapabilitiesPdu> {
Ok(ClipboardCapabilitiesPdu::new(vec![
ClipboardCapability::GeneralCapability(GeneralCapabilitySet {
version: ClipboardProtocolVersion::V2,
general_flags: GeneralCapabilityFlags::default(),
}),
]))
}
fn format_list(&mut self, pdu: &FormatListPdu) -> anyhow::Result<()> {
// Handle incoming format list from peer
for format in &pdu.formats {
println!("Available format: {} ({})", format.id, format.name);
}
Ok(())
}
fn format_data_request(&mut self, pdu: &FormatDataRequestPdu) -> anyhow::Result<FormatDataResponsePdu> {
// Return clipboard data for requested format
Ok(FormatDataResponsePdu {
data: self.content.clone(),
})
}
fn format_data_response(&mut self, pdu: &FormatDataResponsePdu) -> anyhow::Result<()> {
// Receive clipboard data from peer
self.content = pdu.data.clone();
Ok(())
}
}
Audio Output (RDPSND)
Stream audio from server to client.
Server Implementation
use ironrdp::rdpsnd::pdu::*;
use ironrdp::rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage};
use ironrdp::server::{SoundServerFactory, ServerEvent};
struct AudioHandler {
task: Option<tokio::task::JoinHandle<()>>,
}
impl RdpsndServerHandler for AudioHandler {
fn get_formats(&self) -> &[AudioFormat] {
&[
AudioFormat {
format: WaveFormat::PCM,
n_channels: 2,
n_samples_per_sec: 44100,
n_avg_bytes_per_sec: 176400,
n_block_align: 4,
bits_per_sample: 16,
data: None,
},
AudioFormat {
format: WaveFormat::OPUS,
n_channels: 2,
n_samples_per_sec: 48000,
n_avg_bytes_per_sec: 192000,
n_block_align: 4,
bits_per_sample: 16,
data: None,
},
]
}
fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option<u16> {
// Client selected a format, start streaming
let format_index = 0; // Index into get_formats()
// Spawn audio streaming task
self.task = Some(tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(20));
loop {
interval.tick().await;
// Generate and send audio samples
}
}));
Some(format_index)
}
fn stop(&mut self) {
if let Some(task) = self.task.take() {
task.abort();
}
}
}
Sending Audio Frames
use ironrdp::server::ServerEvent;
// From your audio streaming task:
let audio_data = vec![0u8; 1024]; // PCM samples
let timestamp: u16 = 0; // Incrementing timestamp
sender.send(ServerEvent::Rdpsnd(RdpsndServerMessage::Wave(
audio_data,
timestamp,
)))?;
Device Redirection (RDPDR)
Redirect local devices (drives, printers, serial ports) to the remote session.
use ironrdp::rdpdr::pdu::*;
// RDPDR implementation is more complex
// See crates/ironrdp-rdpdr for full details
Implementing Custom Static Channels
Define Channel Trait
use ironrdp_svc::{SvcProcessor, SvcProcessorMessages, SvcMessage};
struct MyCustomChannel {
// Channel state
}
impl SvcProcessor for MyCustomChannel {
fn channel_name(&self) -> &str {
"MYCHAN" // 8 characters max
}
fn process(&mut self, payload: &[u8]) -> anyhow::Result<SvcProcessorMessages> {
// Decode incoming PDU
let message = parse_my_protocol(payload)?;
// Process message
let response = self.handle_message(message)?;
// Return response PDUs
Ok(vec![SvcMessage {
channel_name: self.channel_name().to_owned(),
data: encode_my_protocol(response)?,
}])
}
}
Register Channel
Channels are registered during connection setup through the connector configuration:
use ironrdp::connector::Config;
let mut config = Config {
// ... other config
request_data: Some(vec!["MYCHAN".to_owned()]),
// ...
};
Implementing Dynamic Channels (DVC)
Client-side DVC
use ironrdp::dvc::{DvcProcessor, DvcMessage};
use ironrdp_core::{ReadCursor, WriteBuf};
struct MyDvcChannel;
impl DvcProcessor for MyDvcChannel {
fn channel_name(&self) -> &str {
"my.custom.dvc"
}
fn start(&mut self, channel_id: u32) -> anyhow::Result<()> {
println!("DVC channel started with ID: {}", channel_id);
Ok(())
}
fn process(&mut self, payload: &[u8]) -> anyhow::Result<Vec<DvcMessage>> {
let mut cursor = ReadCursor::new(payload);
// Parse your custom protocol
let message_type = cursor.read_u8()?;
let data_length = cursor.read_u32()?;
let data = cursor.read_slice(data_length as usize)?;
// Handle message
match message_type {
1 => self.handle_data_message(data)?,
2 => self.handle_control_message(data)?,
_ => anyhow::bail!("unknown message type"),
}
// Return response messages
Ok(vec![])
}
fn close(&mut self) -> anyhow::Result<()> {
println!("DVC channel closed");
Ok(())
}
}
Creating DVC Messages
use ironrdp::dvc::DvcMessage;
use ironrdp_core::WriteBuf;
fn create_dvc_message() -> anyhow::Result<DvcMessage> {
let mut buf = WriteBuf::new();
buf.write_u8(1)?; // Message type
buf.write_u32(100)?; // Data length
buf.write_slice(&[0u8; 100])?; // Payload
Ok(DvcMessage {
channel_name: "my.custom.dvc".to_owned(),
data: buf.into_inner(),
})
}
Channel Processing Patterns
Stateful Channel
struct StatefulChannel {
state: ChannelState,
pending_requests: HashMap<u32, PendingRequest>,
}
enum ChannelState {
Initializing,
Ready,
Closed,
}
impl SvcProcessor for StatefulChannel {
fn process(&mut self, payload: &[u8]) -> anyhow::Result<SvcProcessorMessages> {
match self.state {
ChannelState::Initializing => {
// Handle initialization messages
self.state = ChannelState::Ready;
Ok(vec![])
}
ChannelState::Ready => {
// Normal message processing
self.handle_ready_message(payload)
}
ChannelState::Closed => {
anyhow::bail!("channel is closed")
}
}
}
}
Request-Response Channel
struct RequestResponseChannel {
next_request_id: u32,
}
impl RequestResponseChannel {
fn send_request(&mut self, data: Vec<u8>) -> u32 {
let request_id = self.next_request_id;
self.next_request_id += 1;
// Encode request with ID
let mut buf = WriteBuf::new();
buf.write_u32(request_id).unwrap();
buf.write_slice(&data).unwrap();
request_id
}
fn handle_response(&mut self, payload: &[u8]) -> anyhow::Result<()> {
let mut cursor = ReadCursor::new(payload);
let request_id = cursor.read_u32()?;
let response_data = cursor.remaining();
println!("Received response for request {}", request_id);
Ok(())
}
}
Best Practices
Error Handling
use ironrdp_error::{Error, ErrorKind};
#[derive(Debug)]
enum MyChannelError {
InvalidMessage,
ProtocolViolation,
}
impl std::fmt::Display for MyChannelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidMessage => write!(f, "invalid message"),
Self::ProtocolViolation => write!(f, "protocol violation"),
}
}
}
Logging
use tracing::{debug, trace, warn};
impl SvcProcessor for MyChannel {
fn process(&mut self, payload: &[u8]) -> anyhow::Result<SvcProcessorMessages> {
trace!(len = payload.len(), "received channel message");
match self.decode_message(payload) {
Ok(msg) => {
debug!(?msg, "processing message");
self.handle_message(msg)
}
Err(e) => {
warn!(error = %e, "failed to decode message");
Err(e)
}
}
}
}
Buffer Management
Use WriteBuf and ReadCursor for efficient encoding/decoding:
use ironrdp_core::{WriteBuf, ReadCursor};
fn encode_message(msg: &MyMessage) -> anyhow::Result<Vec<u8>> {
let mut buf = WriteBuf::new();
buf.write_u16(msg.message_type)?;
buf.write_u32(msg.data.len() as u32)?;
buf.write_slice(&msg.data)?;
Ok(buf.into_inner())
}
fn decode_message(payload: &[u8]) -> anyhow::Result<MyMessage> {
let mut cursor = ReadCursor::new(payload);
let message_type = cursor.read_u16()?;
let data_len = cursor.read_u32()? as usize;
let data = cursor.read_slice(data_len)?.to_vec();
Ok(MyMessage { message_type, data })
}
Next Steps