Overview
The ironrdp-svc crate provides traits and utilities for implementing RDP Static Virtual Channels (SVCs). Static channels are:
- Established during connection setup (up to 31 custom channels)
- Lossless, ordered byte streams
- Used for clipboard, file transfer, audio, device redirection, etc.
This crate handles:
- PDU chunkification (splitting large messages into ~1600 byte chunks)
- Channel framing headers
- MCS/X.224 wrapping
Core Traits
SvcProcessor
pub trait SvcProcessor: AsAny + fmt::Debug + Send {
fn channel_name(&self) -> ChannelName;
fn compression_condition(&self) -> CompressionCondition;
fn start(&mut self) -> PduResult<Vec<SvcMessage>>;
fn process(&mut self, payload: &[u8]) -> PduResult<Vec<SvcMessage>>;
}
Implement this trait to create a custom static virtual channel.
Methods:
Returns the 7-character channel name (e.g., “CLIPRDR”, “RDPDR”)
compression_condition
fn(&self) -> CompressionCondition
Defines when channel data should be compressed (default: Never)
start
fn(&mut self) -> PduResult<Vec<SvcMessage>>
Called after the channel is joined. Returns initial PDUs to send (default: empty)
process
fn(&mut self, payload: &[u8]) -> PduResult<Vec<SvcMessage>>
Process a received PDU (fully de-chunkified). Returns response PDUs to send.
SvcClientProcessor & SvcServerProcessor
pub trait SvcClientProcessor: SvcProcessor {}
pub trait SvcServerProcessor: SvcProcessor {}
Marker traits to distinguish client and server channel implementations.
Core Types
StaticVirtualChannel
pub struct StaticVirtualChannel { /* ... */ }
impl StaticVirtualChannel {
pub fn new<T: SvcProcessor + 'static>(channel_processor: T) -> Self;
pub fn channel_name(&self) -> ChannelName;
pub fn compression_condition(&self) -> CompressionCondition;
pub fn start(&mut self) -> PduResult<Vec<SvcMessage>>;
pub fn process(&mut self, payload: &[u8]) -> PduResult<Vec<SvcMessage>>;
pub fn chunkify(messages: Vec<SvcMessage>) -> EncodeResult<Vec<WriteBuf>>;
pub fn channel_processor_downcast_ref<T: SvcProcessor + 'static>(&self) -> Option<&T>;
pub fn channel_processor_downcast_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T>;
}
Wrapper around a SvcProcessor that handles chunking and framing.
SvcMessage
pub struct SvcMessage { /* ... */ }
impl SvcMessage {
pub fn with_flags(self, flags: ChannelFlags) -> Self;
pub fn encode_unframed_pdu(&self) -> EncodeResult<Vec<u8>>;
}
impl<T: SvcEncode + 'static> From<T> for SvcMessage { /* ... */ }
A message to send over a static virtual channel. Automatically converted from any SvcEncode type.
SvcEncode
pub trait SvcEncode: Encode + Send {}
Marker trait for types that can be sent as SVC messages. Automatically implemented for types that implement Encode.
Example:
use ironrdp_core::{Encode, WriteCursor, EncodeResult};
use ironrdp_svc::SvcEncode;
struct MyChannelPdu {
field1: u32,
field2: u16,
}
impl Encode for MyChannelPdu {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
dst.write_u32(self.field1);
dst.write_u16(self.field2);
Ok(())
}
fn name(&self) -> &'static str { "MyChannelPdu" }
fn size(&self) -> usize { 6 }
}
impl SvcEncode for MyChannelPdu {}
StaticChannelSet
pub struct StaticChannelSet { /* ... */ }
impl StaticChannelSet {
pub fn new() -> Self;
pub fn insert<T: SvcProcessor + 'static>(&mut self, val: T) -> Option<StaticVirtualChannel>;
pub fn get_by_type<T: SvcProcessor + 'static>(&self) -> Option<&StaticVirtualChannel>;
pub fn get_by_type_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut StaticVirtualChannel>;
pub fn get_by_channel_id(&self, channel_id: StaticChannelId) -> Option<&StaticVirtualChannel>;
pub fn get_by_channel_id_mut(&mut self, channel_id: StaticChannelId) -> Option<&mut StaticVirtualChannel>;
pub fn get_by_channel_name(&self, name: &ChannelName) -> Option<(TypeId, &StaticVirtualChannel)>;
pub fn attach_channel_id(&mut self, type_id: TypeId, channel_id: StaticChannelId) -> Option<StaticChannelId>;
pub fn iter(&self) -> impl Iterator<Item = (TypeId, &StaticVirtualChannel)>;
pub fn values(&self) -> impl Iterator<Item = &StaticVirtualChannel>;
pub fn channel_ids(&self) -> impl Iterator<Item = StaticChannelId>;
}
A set holding at most one channel per SvcProcessor type. Maps between:
- Type IDs (from
TypeId::of::<T>())
- Channel names
- Channel IDs (assigned by the server during connection)
CompressionCondition
pub enum CompressionCondition {
Never,
WhenRdpDataIsCompressed,
Always,
}
Defines when virtual channel data should be compressed.
Channel Encoding
Client-Side Encoding
pub fn client_encode_svc_messages(
messages: Vec<SvcMessage>,
channel_id: u16,
initiator_id: u16,
) -> EncodeResult<Vec<u8>>
Encode messages for sending from client to server. Handles:
- Chunkifying messages
- Adding Channel PDU Headers
- Wrapping in MCS Send Data Request
- Adding X.224 and TPKT framing
Returns fully encoded bytes ready to send over the transport.
Server-Side Encoding
pub fn server_encode_svc_messages(
messages: Vec<SvcMessage>,
channel_id: u16,
initiator_id: u16,
) -> EncodeResult<Vec<u8>>
Same as client encoding but uses MCS Send Data Indication.
Channel Definitions
make_channel_definition
pub fn make_channel_definition(channel: &StaticVirtualChannel) -> ChannelDef;
pub fn make_channel_options(channel: &StaticVirtualChannel) -> ChannelOptions;
Create the ChannelDef structure to advertise during connection negotiation.
Implementation Example
use ironrdp_svc::*;
use ironrdp_pdu::gcc::ChannelName;
use ironrdp_core::{Encode, WriteCursor, EncodeResult};
#[derive(Debug)]
struct MyChannel {
state: u32,
}
impl MyChannel {
fn new() -> Self {
Self { state: 0 }
}
}
ironrdp_core::impl_as_any!(MyChannel);
impl SvcProcessor for MyChannel {
fn channel_name(&self) -> ChannelName {
ChannelName::from_bytes(b"MYCHAN\x00").unwrap()
}
fn start(&mut self) -> PduResult<Vec<SvcMessage>> {
// Send initial greeting
Ok(vec![MyGreetingPdu { version: 1 }.into()])
}
fn process(&mut self, payload: &[u8]) -> PduResult<Vec<SvcMessage>> {
// Parse PDU from payload
let pdu = decode_my_pdu(payload)?;
// Update state
self.state += 1;
// Return response PDUs
Ok(vec![MyResponsePdu { state: self.state }.into()])
}
}
impl SvcClientProcessor for MyChannel {}
struct MyGreetingPdu { version: u32 }
impl Encode for MyGreetingPdu {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
dst.write_u32(self.version);
Ok(())
}
fn name(&self) -> &'static str { "MyGreetingPdu" }
fn size(&self) -> usize { 4 }
}
impl SvcEncode for MyGreetingPdu {}
struct MyResponsePdu { state: u32 }
impl Encode for MyResponsePdu {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
dst.write_u32(self.state);
Ok(())
}
fn name(&self) -> &'static str { "MyResponsePdu" }
fn size(&self) -> usize { 4 }
}
impl SvcEncode for MyResponsePdu {}
Usage Example
use ironrdp_svc::*;
use std::any::TypeId;
// Create channel set
let mut channels = StaticChannelSet::new();
channels.insert(MyChannel::new());
// Get channel definitions for connection negotiation
let defs: Vec<_> = channels.values()
.map(make_channel_definition)
.collect();
// After connection, attach server-assigned IDs
channels.attach_channel_id(TypeId::of::<MyChannel>(), 1003);
// Process received data
if let Some(channel) = channels.get_by_channel_id_mut(1003) {
let responses = channel.process(&received_data)?;
// Encode responses
let encoded = client_encode_svc_messages(responses, 1003, user_id)?;
// ... send encoded bytes
}
Channel Framing
ChannelFlags
bitflags! {
pub struct ChannelFlags: u32 {
const FIRST = 0x0000_0001;
const LAST = 0x0000_0002;
const SHOW_PROTOCOL = 0x0000_0010;
const SUSPEND = 0x0000_0020;
const RESUME = 0x0000_0040;
const SHADOW_PERSISTENT = 0x0000_0080;
const COMPRESSED = 0x0020_0000;
const AT_FRONT = 0x0040_0000;
const FLUSHED = 0x0080_0000;
}
}
Flags in the Channel PDU Header. FIRST and LAST are set automatically during chunkification.
Chunk Size
pub const CHANNEL_CHUNK_LENGTH: usize = 1600;
Default maximum chunk size. Servers may advertise a larger size in capability sets.
Re-exports
pub use ironrdp_pdu as pdu; // Convenient access to PDU types
Usage Notes
Automatic Chunkification:
You don’t need to manually chunk messages. StaticVirtualChannel handles it:let large_pdu = MyPdu { data: vec![0u8; 10000] };
let messages = vec![large_pdu.into()];
let chunks = StaticVirtualChannel::chunkify(messages)?;
// Creates ~7 chunks with appropriate FIRST/LAST flags
Type-Safe Channel Access:
Use StaticChannelSet to safely manage multiple channels:let cliprdr = channels.get_by_type::<ClipboardChannel>();
let rdpdr = channels.get_by_type::<DeviceRedirectionChannel>();
No string comparisons or manual casting needed.
Channel Limits:
RDP supports up to 31 custom static virtual channels plus the mandatory I/O channel (32 total). Choose channel names carefully.
See Also