Skip to content

Commit 5482365

Browse files
authored
feat(dvc): add DVC named pipe proxy support (#791)
1 parent e5f92ae commit 5482365

File tree

26 files changed

+1270
-17
lines changed

26 files changed

+1270
-17
lines changed

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.3" }
4848
ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" }
4949
ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.5", features = ["reqwest"] }
5050
ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath"
51+
ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy"
5152

5253
# Windowing and rendering
5354
winit = { version = "0.30", features = ["rwh_06"] }

crates/ironrdp-client/src/config.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ pub struct Config {
2121
pub connector: connector::Config,
2222
pub clipboard_type: ClipboardType,
2323
pub rdcleanpath: Option<RDCleanPathConfig>,
24+
25+
/// DVC channel <-> named pipe proxy configuration.
26+
///
27+
/// Each configured proxy enables IronRDP to connect to DVC channel and create a named pipe
28+
/// server, which will be used for proxying DVC messages to/from user-defined DVC logic
29+
/// implemented as named pipe clients (either in the same process or in a different process).
30+
pub dvc_pipe_proxies: Vec<DvcProxyInfo>,
2431
}
2532

2633
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@@ -137,6 +144,33 @@ pub struct RDCleanPathConfig {
137144
pub auth_token: String,
138145
}
139146

147+
#[derive(Clone, Debug)]
148+
pub struct DvcProxyInfo {
149+
pub channel_name: String,
150+
pub pipe_name: String,
151+
}
152+
153+
impl FromStr for DvcProxyInfo {
154+
type Err = anyhow::Error;
155+
156+
fn from_str(s: &str) -> Result<Self, Self::Err> {
157+
let mut parts = s.split('=');
158+
let channel_name = parts
159+
.next()
160+
.ok_or_else(|| anyhow::anyhow!("missing DVC channel name"))?
161+
.to_owned();
162+
let pipe_name = parts
163+
.next()
164+
.ok_or_else(|| anyhow::anyhow!("missing DVC proxy pipe name"))?
165+
.to_owned();
166+
167+
Ok(Self {
168+
channel_name,
169+
pipe_name,
170+
})
171+
}
172+
}
173+
140174
/// Devolutions IronRDP client
141175
#[derive(Parser, Debug)]
142176
#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")]
@@ -238,6 +272,14 @@ struct Args {
238272
/// The bitmap codecs to use (remotefx:on, ...)
239273
#[clap(long, value_parser, num_args = 1.., value_delimiter = ',')]
240274
codecs: Vec<String>,
275+
276+
/// Add DVC channel named pipe proxy.
277+
/// the format is <name>=<pipe>
278+
/// e.g. `ChannelName=PipeName` where `ChannelName` is the name of the channel,
279+
/// and `PipeName` is the name of the named pipe to connect to (without OS-specific prefix),
280+
/// e.g. PipeName will automatically be prefixed with `\\.\pipe\` on Windows.
281+
#[clap(long, value_parser)]
282+
dvc_proxy: Vec<DvcProxyInfo>,
241283
}
242284

243285
impl Config {
@@ -357,6 +399,7 @@ impl Config {
357399
connector,
358400
clipboard_type,
359401
rdcleanpath,
402+
dvc_pipe_proxies: args.dvc_proxy,
360403
})
361404
}
362405
}

crates/ironrdp-client/src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ extern crate tracing;
66
use anyhow::Context as _;
77
use ironrdp_client::app::App;
88
use ironrdp_client::config::{ClipboardType, Config};
9-
use ironrdp_client::rdp::{RdpClient, RdpInputEvent, RdpOutputEvent};
9+
use ironrdp_client::rdp::{DvcPipeProxyFactory, RdpClient, RdpInputEvent, RdpOutputEvent};
1010
use tokio::runtime;
1111
use winit::event_loop::EventLoop;
1212

@@ -50,7 +50,7 @@ fn main() -> anyhow::Result<()> {
5050
use ironrdp_client::clipboard::ClientClipboardMessageProxy;
5151
use ironrdp_cliprdr_native::WinClipboard;
5252

53-
let cliprdr = WinClipboard::new(ClientClipboardMessageProxy::new(input_event_sender))?;
53+
let cliprdr = WinClipboard::new(ClientClipboardMessageProxy::new(input_event_sender.clone()))?;
5454

5555
let factory = cliprdr.backend_factory();
5656
_win_clipboard = cliprdr;
@@ -59,11 +59,14 @@ fn main() -> anyhow::Result<()> {
5959
_ => None,
6060
};
6161

62+
let dvc_pipe_proxy_factory = DvcPipeProxyFactory::new(input_event_sender);
63+
6264
let client = RdpClient {
6365
config,
6466
event_loop_proxy,
6567
input_event_receiver,
6668
cliprdr_factory,
69+
dvc_pipe_proxy_factory,
6770
};
6871

6972
debug!("Start RDP thread");

crates/ironrdp-client/src/rdp.rs

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ use ironrdp::displaycontrol::pdu::MonitorLayoutEntry;
88
use ironrdp::graphics::image_processing::PixelFormat;
99
use ironrdp::graphics::pointer::DecodedPointer;
1010
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
11+
use ironrdp::pdu::{pdu_other_err, PduResult};
1112
use ironrdp::session::image::DecodedImage;
1213
use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDisconnectReason, SessionResult};
14+
use ironrdp::svc::SvcMessage;
1315
use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session};
1416
use ironrdp_core::WriteBuf;
1517
use ironrdp_rdpsnd_native::cpal;
@@ -23,6 +25,7 @@ use tokio::sync::mpsc;
2325
use winit::event_loop::EventLoopProxy;
2426

2527
use crate::config::{Config, RDCleanPathConfig};
28+
use ironrdp_dvc_pipe_proxy::DvcNamedPipeProxy;
2629

2730
#[derive(Debug)]
2831
pub enum RdpOutputEvent {
@@ -47,6 +50,10 @@ pub enum RdpInputEvent {
4750
FastPath(SmallVec<[FastPathInputEvent; 2]>),
4851
Close,
4952
Clipboard(ClipboardMessage),
53+
SendDvcMessages {
54+
channel_id: u32,
55+
messages: Vec<SvcMessage>,
56+
},
5057
}
5158

5259
impl RdpInputEvent {
@@ -55,26 +62,64 @@ impl RdpInputEvent {
5562
}
5663
}
5764

65+
pub struct DvcPipeProxyFactory {
66+
rdp_input_sender: mpsc::UnboundedSender<RdpInputEvent>,
67+
}
68+
69+
impl DvcPipeProxyFactory {
70+
pub fn new(rdp_input_sender: mpsc::UnboundedSender<RdpInputEvent>) -> Self {
71+
Self { rdp_input_sender }
72+
}
73+
74+
pub fn create(&self, channel_name: String, pipe_name: String) -> DvcNamedPipeProxy {
75+
let rdp_input_sender = self.rdp_input_sender.clone();
76+
77+
DvcNamedPipeProxy::new(&channel_name, &pipe_name, move |channel_id, messages| {
78+
rdp_input_sender
79+
.send(RdpInputEvent::SendDvcMessages { channel_id, messages })
80+
.map_err(|_error| pdu_other_err!("send DVC messages to the event loop",))?;
81+
82+
Ok(())
83+
})
84+
}
85+
}
86+
87+
pub type WriteDvcMessageFn = Box<dyn Fn(u32, SvcMessage) -> PduResult<()> + Send + 'static>;
88+
5889
pub struct RdpClient {
5990
pub config: Config,
6091
pub event_loop_proxy: EventLoopProxy<RdpOutputEvent>,
6192
pub input_event_receiver: mpsc::UnboundedReceiver<RdpInputEvent>,
6293
pub cliprdr_factory: Option<Box<dyn CliprdrBackendFactory + Send>>,
94+
pub dvc_pipe_proxy_factory: DvcPipeProxyFactory,
6395
}
6496

6597
impl RdpClient {
6698
pub async fn run(mut self) {
6799
loop {
68100
let (connection_result, framed) = if let Some(rdcleanpath) = self.config.rdcleanpath.as_ref() {
69-
match connect_ws(&self.config, rdcleanpath, self.cliprdr_factory.as_deref()).await {
101+
match connect_ws(
102+
&self.config,
103+
rdcleanpath,
104+
self.cliprdr_factory.as_deref(),
105+
&self.dvc_pipe_proxy_factory,
106+
)
107+
.await
108+
{
70109
Ok(result) => result,
71110
Err(e) => {
72111
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e));
73112
break;
74113
}
75114
}
76115
} else {
77-
match connect(&self.config, self.cliprdr_factory.as_deref()).await {
116+
match connect(
117+
&self.config,
118+
self.cliprdr_factory.as_deref(),
119+
&self.dvc_pipe_proxy_factory,
120+
)
121+
.await
122+
{
78123
Ok(result) => result,
79124
Err(e) => {
80125
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e));
@@ -122,6 +167,7 @@ type UpgradedFramed = ironrdp_tokio::TokioFramed<Box<dyn AsyncReadWrite + Unpin
122167
async fn connect(
123168
config: &Config,
124169
cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>,
170+
dvc_pipe_proxy_factory: &DvcPipeProxyFactory,
125171
) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> {
126172
let dest = format!("{}:{}", config.destination.name(), config.destination.port());
127173

@@ -135,10 +181,21 @@ async fn connect(
135181

136182
let mut framed = ironrdp_tokio::TokioFramed::new(socket);
137183

184+
let mut drdynvc =
185+
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));
186+
187+
// Instantiate all DVC proxies
188+
for proxy in config.dvc_pipe_proxies.iter() {
189+
let channel_name = proxy.channel_name.clone();
190+
let pipe_name = proxy.pipe_name.clone();
191+
192+
trace!(%channel_name, %pipe_name, "Creating DVC proxy");
193+
194+
drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name));
195+
}
196+
138197
let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr)
139-
.with_static_channel(
140-
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))),
141-
)
198+
.with_static_channel(drdynvc)
142199
.with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new())))
143200
.with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0));
144201

@@ -186,6 +243,7 @@ async fn connect_ws(
186243
config: &Config,
187244
rdcleanpath: &RDCleanPathConfig,
188245
cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>,
246+
dvc_pipe_proxy_factory: &DvcPipeProxyFactory,
189247
) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> {
190248
let hostname = rdcleanpath
191249
.url
@@ -214,10 +272,21 @@ async fn connect_ws(
214272

215273
let mut framed = ironrdp_tokio::TokioFramed::new(ws);
216274

275+
let mut drdynvc =
276+
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));
277+
278+
// Instantiate all DVC proxies
279+
for proxy in config.dvc_pipe_proxies.iter() {
280+
let channel_name = proxy.channel_name.clone();
281+
let pipe_name = proxy.pipe_name.clone();
282+
283+
trace!(%channel_name, %pipe_name, "Creating DVC proxy");
284+
285+
drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name));
286+
}
287+
217288
let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr)
218-
.with_static_channel(
219-
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))),
220-
)
289+
.with_static_channel(drdynvc)
221290
.with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new())))
222291
.with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0));
223292

@@ -468,6 +537,12 @@ async fn active_session(
468537
Vec::new()
469538
}
470539
}
540+
RdpInputEvent::SendDvcMessages { channel_id, messages } => {
541+
trace!(channel_id, ?messages, "Send DVC messages");
542+
543+
let frame = active_stage.encode_dvc_messages(messages)?;
544+
vec![ActiveStageOutput::ResponseFrame(frame)]
545+
}
471546
}
472547
}
473548
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[package]
2+
name = "ironrdp-dvc-pipe-proxy"
3+
version = "0.1.0"
4+
readme = "README.md"
5+
description = "DVC named pipe proxy for IronRDP"
6+
edition.workspace = true
7+
license.workspace = true
8+
homepage.workspace = true
9+
repository.workspace = true
10+
authors.workspace = true
11+
keywords.workspace = true
12+
categories.workspace = true
13+
14+
[lib]
15+
doctest = false
16+
test = false
17+
18+
[dependencies]
19+
ironrdp-core.path = "../ironrdp-core"
20+
ironrdp-dvc.path = "../ironrdp-dvc"
21+
ironrdp-pdu.path = "../ironrdp-pdu"
22+
ironrdp-svc.path = "../ironrdp-svc"
23+
24+
tracing = { version = "0.1", features = ["log"] }
25+
26+
27+
[target.'cfg(windows)'.dependencies]
28+
widestring = "1"
29+
windows = { version = "0.61", features = [
30+
"Win32_Foundation",
31+
"Win32_Security",
32+
"Win32_System_Threading",
33+
"Win32_Storage_FileSystem",
34+
"Win32_System_Pipes",
35+
"Win32_System_IO",
36+
] }
37+
38+
39+
[lints]
40+
workspace = true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-APACHE
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-MIT
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# IronRDP DVC pipe proxy
2+
3+
This crate provides a Device Virtual Channel (DVC) handler for IronRDP, enabling proxying of RDP DVC
4+
traffic over a named pipe.
5+
6+
It was originally designed to simplify custom DVC integration within Devolutions Remote Desktop
7+
Manager (RDM). By implementing a thin pipe proxy for target RDP clients (such as IronRDP, FreeRDP,
8+
mstsc, etc.), the main client logic can be centralized and reused across all supported clients via a
9+
named pipe.
10+
11+
This approach allows you to implement your DVC logic in one place, making it easier to support
12+
multiple RDP clients without duplicating code.
13+
14+
Additionally, this crate can be used for other scenarios, such as testing your own custom DVC
15+
channel client, without needing to patch or rebuild IronRDP itself.

0 commit comments

Comments
 (0)