State Machine Architecture
The connection process is split across two primary crates:ironrdp-connector: Handles connection establishment up to the “Connected” stateironrdp-session: Manages the active RDP session after connection
Why State Machines?
IronRDP’s state machine design enforces several architectural invariants:- No hidden I/O: Core tier crates never perform network operations
- Explicit state transitions: Each step is visible and controllable
- Testability: States can be tested in isolation
- Async-agnostic: Works with blocking, async, or custom I/O strategies
ironrdp-connector/src/lib.rs:327-337.
Connection States
TheClientConnector state machine progresses through these states:
State Diagram
Step-by-Step Connection Flow
use ironrdp_connector::{ClientConnector, Config, DesktopSize, Credentials};
use std::net::{SocketAddr, IpAddr, Ipv4Addr};
let config = Config {
desktop_size: DesktopSize { width: 1920, height: 1080 },
desktop_scale_factor: 100,
enable_tls: true,
enable_credssp: true,
credentials: Credentials::UsernamePassword {
username: "admin".to_string(),
password: "P@ssw0rd".to_string(),
},
domain: Some("CORP".to_string()),
client_build: 7601,
client_name: "ironrdp-client".to_string(),
// ... (other fields with defaults)
};
let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 0);
let mut connector = ClientConnector::new(config, client_addr);
use ironrdp_core::WriteBuf;
use ironrdp_connector::{Sequence, Written};
let mut output = WriteBuf::new();
let written = connector.step(&[], &mut output)?;
if let Written::Size(size) = written {
let request_pdu = output.filled();
// Send request_pdu to server via TCP socket
stream.write_all(request_pdu)?;
}
// ironrdp-connector/src/connection.rs:245-291
let mut security_protocol = nego::SecurityProtocol::empty();
if self.config.enable_tls {
security_protocol.insert(nego::SecurityProtocol::SSL);
}
if self.config.enable_credssp {
security_protocol.insert(
nego::SecurityProtocol::HYBRID | nego::SecurityProtocol::HYBRID_EX
);
}
let connection_request = nego::ConnectionRequest {
nego_data: /* cookie or None */,
flags: nego::RequestFlags::empty(),
protocol: security_protocol,
};
let mut input_buf = vec![0u8; 4096];
let bytes_read = stream.read(&mut input_buf)?;
let input = &input_buf[..bytes_read];
let written = connector.step(input, &mut output)?;
// ironrdp-connector/src/connection.rs:293-323
let connection_confirm = decode::<X224<nego::ConnectionConfirm>>(input)?;
let (flags, selected_protocol) = match connection_confirm {
nego::ConnectionConfirm::Response { flags, protocol } => (flags, protocol),
nego::ConnectionConfirm::Failure { code } => {
return Err(/* NegotiationFailure error */);
}
};
// Verify server selected a protocol we offered
if !selected_protocol.intersects(requested_protocol) {
return Err(/* protocol mismatch error */);
}
if connector.should_perform_security_upgrade() {
// Perform TLS handshake on the TCP stream
let tls_stream = tls_connector.connect(server_name, stream)?;
// Mark upgrade as complete
connector.mark_security_upgrade_as_done();
// Update stream reference
stream = tls_stream;
}
if connector.should_perform_credssp() {
// Perform CredSSP authentication using the sspi crate
// This is also handled by user code
perform_credssp_auth(&mut stream, &config.credentials)?;
connector.mark_credssp_as_done();
}
IronRDP exposes
sspi re-exports but leaves CredSSP execution to user code. See ironrdp-connector/src/credssp.rs for reference.let written = connector.step(&[], &mut output)?;
if let Written::Size(_) = written {
stream.write_all(output.filled())?;
}
ClientCoreData: Desktop size, color depth, keyboard layout, client name, RDP versionClientSecurityData: Encryption methods (empty for Enhanced RDP Security)ClientNetworkData: List of static virtual channels to open// ironrdp-connector/src/connection.rs:668-728
ClientGccBlocks {
core: ClientCoreData {
version: RdpVersion::V5_PLUS,
desktop_width: config.desktop_size.width,
desktop_height: config.desktop_size.height,
keyboard_layout: config.keyboard_layout,
client_name: config.client_name.clone(),
optional_data: ClientCoreOptionalData {
supported_color_depths: Some(supported_color_depths),
early_capability_flags: Some(early_capability_flags),
server_selected_protocol: Some(selected_protocol),
// ...
},
},
security: ClientSecurityData { /* ... */ },
network: Some(ClientNetworkData { channels }),
// ...
}
ServerCoreData: RDP version, early capability flagsServerSecurityData: Server random, certificates (if applicable)ServerNetworkData: Assigned MCS channel IDs for static channelsoutput.clear();
let bytes_read = stream.read(&mut input_buf)?;
let written = connector.step(&input_buf[..bytes_read], &mut output)?;
// ironrdp-connector/src/connection.rs:401-409
let zipped: Vec<_> = self
.static_channels
.type_ids()
.zip(static_channel_ids.iter().copied())
.collect();
zipped.into_iter().for_each(|(channel, channel_id)| {
self.static_channels.attach_channel_id(channel, channel_id);
});
while !matches!(connector.state(), ClientConnectorState::SecureSettingsExchange { .. }) {
output.clear();
let written = connector.step(&[], &mut output)?;
if let Written::Size(_) = written {
stream.write_all(output.filled())?;
let bytes_read = stream.read(&mut input_buf)?;
connector.step(&input_buf[..bytes_read], &mut output)?;
}
}
This step can be skipped if the server advertises
ServerEarlyCapabilityFlags::SKIP_CHANNELJOIN_SUPPORTED.// ironrdp-connector/src/connection.rs:741-815
let client_info = ClientInfo {
credentials: Credentials {
username: config.credentials.username().unwrap_or("").to_owned(),
password: config.credentials.secret().to_owned(),
domain: config.domain.clone(),
},
flags: flags, // Includes COMPRESSION if compression_type is set
compression_type: config.compression_type.unwrap_or(CompressionType::K8),
alternate_shell: config.alternate_shell.clone(),
work_dir: config.work_dir.clone(),
extra_info: ExtendedClientInfo {
address: client_addr.ip().to_string(),
dir: config.client_dir.clone(),
optional_data: ExtendedClientOptionalInfo::builder()
.timezone(config.timezone_info.clone())
.performance_flags(config.performance_flags)
.build(),
},
};
State transitions through:
ConnectTimeAutoDetection → LicensingExchange → MultitransportBootstrappinglet license_exchange = LicenseExchangeSequence::new(
io_channel_id,
username.to_owned(),
domain.clone(),
hardware_id,
license_cache, // Arc<dyn LicenseCache>
);
pub trait LicenseCache: Send + Sync {
fn get(&self, server_name: &str) -> Option<Vec<u8>>;
fn store(&self, server_name: &str, license: Vec<u8>);
}
loop {
output.clear();
let bytes_read = stream.read(&mut input_buf)?;
let written = connector.step(&input_buf[..bytes_read], &mut output)?;
if let Written::Size(_) = written {
stream.write_all(output.filled())?;
}
if connector.state().is_terminal() {
break;
}
}
use ironrdp_connector::{ClientConnectorState, ConnectionResult};
let result = match connector.state {
ClientConnectorState::Connected { result } => result,
_ => panic!("Expected Connected state"),
};
let ConnectionResult {
io_channel_id,
user_channel_id,
share_id,
static_channels,
desktop_size,
connection_activation,
compression_type,
..
} = result;
Active Session Management
After connection, useironrdp-session::ActiveStage to handle the active session:
ironrdp-session/src/active_stage.rs for the complete ActiveStageOutput enum.
Adding Static Virtual Channels
Register static channels before stepping through the connector:SvcClientProcessor:
Error Handling
The connector returnsConnectorResult<T> for all operations:
ironrdp-connector/src/lib.rs:35-82 for NegotiationFailure definitions.
Best Practices
Do:
- Check
connector.state().is_terminal()to know when connection is complete - Use
connector.next_pdu_hint()to determine expected PDU size for buffer allocation - Clear
WriteBufbetween steps to avoid accumulating data - Handle all
ConnectorErrorvariants appropriately
- Call
step()when in a terminal state - Reuse the same input buffer without clearing between reads
- Assume specific byte sizes — always check
Writtenresult - Skip security upgrade or CredSSP steps when indicated by state
Example: Complete Connection Loop
References
- State Machine Trait:
ironrdp-connector/src/lib.rs:327-337 - Connector Implementation:
ironrdp-connector/src/connection.rs - Channel Connection:
ironrdp-connector/src/channel_connection.rs - License Exchange:
ironrdp-connector/src/license_exchange.rs - Connection Activation:
ironrdp-connector/src/connection_activation.rs - Active Session:
ironrdp-session/src/active_stage.rs

