Skip to content

feat(dvc): add DVC named pipe proxy support #791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ironrdp-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.2" }
ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" }
ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.3", features = ["reqwest"] }
ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath"
ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy"

# Windowing and rendering
winit = { version = "0.30", features = ["rwh_06"] }
Expand Down
43 changes: 43 additions & 0 deletions crates/ironrdp-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub struct Config {
pub connector: connector::Config,
pub clipboard_type: ClipboardType,
pub rdcleanpath: Option<RDCleanPathConfig>,

/// DVC channel <-> named pipe proxy configuration.
///
/// Each configured proxy enables IronRDP to connect to DVC channel and create a named pipe
/// server, which will be used for proxying DVC messages to/from user-defined DVC logic
/// implemented as named pipe clients (either in the same process or in a different process).
pub dvc_pipe_proxies: Vec<DvcProxyInfo>,
}

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

#[derive(Clone, Debug)]
pub struct DvcProxyInfo {
pub channel_name: String,
pub pipe_name: String,
}

impl FromStr for DvcProxyInfo {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('=');
let channel_name = parts
.next()
.ok_or_else(|| anyhow::anyhow!("missing DVC channel name"))?
.to_owned();
let pipe_name = parts
.next()
.ok_or_else(|| anyhow::anyhow!("missing DVC proxy pipe name"))?
.to_owned();

Ok(Self {
channel_name,
pipe_name,
})
}
}

/// Devolutions IronRDP client
#[derive(Parser, Debug)]
#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")]
Expand Down Expand Up @@ -238,6 +272,14 @@ struct Args {
/// The bitmap codecs to use (remotefx:on, ...)
#[clap(long, value_parser, num_args = 1.., value_delimiter = ',')]
codecs: Vec<String>,

/// Add DVC channel named pipe proxy.
/// the format is <name>=<pipe>
/// e.g. `ChannelName=PipeName` where `ChannelName` is the name of the channel,
/// and `PipeName` is the name of the named pipe to connect to (without OS-specific prefix),
/// e.g. PipeName will automatically be prefixed with `\\.\pipe\` on Windows.
#[clap(long, value_parser)]
dvc_proxy: Vec<DvcProxyInfo>,
}

impl Config {
Expand Down Expand Up @@ -357,6 +399,7 @@ impl Config {
connector,
clipboard_type,
rdcleanpath,
dvc_pipe_proxies: args.dvc_proxy,
})
}
}
7 changes: 5 additions & 2 deletions crates/ironrdp-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extern crate tracing;
use anyhow::Context as _;
use ironrdp_client::app::App;
use ironrdp_client::config::{ClipboardType, Config};
use ironrdp_client::rdp::{RdpClient, RdpInputEvent, RdpOutputEvent};
use ironrdp_client::rdp::{DvcPipeProxyFactory, RdpClient, RdpInputEvent, RdpOutputEvent};
use tokio::runtime;
use winit::event_loop::EventLoop;

Expand Down Expand Up @@ -50,7 +50,7 @@ fn main() -> anyhow::Result<()> {
use ironrdp_client::clipboard::ClientClipboardMessageProxy;
use ironrdp_cliprdr_native::WinClipboard;

let cliprdr = WinClipboard::new(ClientClipboardMessageProxy::new(input_event_sender))?;
let cliprdr = WinClipboard::new(ClientClipboardMessageProxy::new(input_event_sender.clone()))?;

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

let dvc_pipe_proxy_factory = DvcPipeProxyFactory::new(input_event_sender);

let client = RdpClient {
config,
event_loop_proxy,
input_event_receiver,
cliprdr_factory,
dvc_pipe_proxy_factory,
};

debug!("Start RDP thread");
Expand Down
91 changes: 83 additions & 8 deletions crates/ironrdp-client/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ use ironrdp::displaycontrol::client::DisplayControlClient;
use ironrdp::displaycontrol::pdu::MonitorLayoutEntry;
use ironrdp::graphics::image_processing::PixelFormat;
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
use ironrdp::pdu::{pdu_other_err, PduResult};
use ironrdp::session::image::DecodedImage;
use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDisconnectReason, SessionResult};
use ironrdp::svc::SvcMessage;
use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session};
use ironrdp_core::WriteBuf;
use ironrdp_rdpsnd_native::cpal;
Expand All @@ -20,6 +22,7 @@ use tokio::sync::mpsc;
use winit::event_loop::EventLoopProxy;

use crate::config::{Config, RDCleanPathConfig};
use ironrdp_dvc_pipe_proxy::DvcNamedPipeProxy;

#[derive(Debug)]
pub enum RdpOutputEvent {
Expand All @@ -43,6 +46,10 @@ pub enum RdpInputEvent {
FastPath(SmallVec<[FastPathInputEvent; 2]>),
Close,
Clipboard(ClipboardMessage),
SendDvcMessages {
channel_id: u32,
messages: Vec<SvcMessage>,
},
}

impl RdpInputEvent {
Expand All @@ -51,26 +58,64 @@ impl RdpInputEvent {
}
}

pub struct DvcPipeProxyFactory {
rdp_input_sender: mpsc::UnboundedSender<RdpInputEvent>,
}

impl DvcPipeProxyFactory {
pub fn new(rdp_input_sender: mpsc::UnboundedSender<RdpInputEvent>) -> Self {
Self { rdp_input_sender }
}

pub fn create(&self, channel_name: String, pipe_name: String) -> DvcNamedPipeProxy {
let rdp_input_sender = self.rdp_input_sender.clone();

DvcNamedPipeProxy::new(&channel_name, &pipe_name, move |channel_id, messages| {
rdp_input_sender
.send(RdpInputEvent::SendDvcMessages { channel_id, messages })
.map_err(|_error| pdu_other_err!("send DVC messages to the event loop",))?;

Ok(())
})
}
}

pub type WriteDvcMessageFn = Box<dyn Fn(u32, SvcMessage) -> PduResult<()> + Send + 'static>;

pub struct RdpClient {
pub config: Config,
pub event_loop_proxy: EventLoopProxy<RdpOutputEvent>,
pub input_event_receiver: mpsc::UnboundedReceiver<RdpInputEvent>,
pub cliprdr_factory: Option<Box<dyn CliprdrBackendFactory + Send>>,
pub dvc_pipe_proxy_factory: DvcPipeProxyFactory,
}

impl RdpClient {
pub async fn run(mut self) {
loop {
let (connection_result, framed) = if let Some(rdcleanpath) = self.config.rdcleanpath.as_ref() {
match connect_ws(&self.config, rdcleanpath, self.cliprdr_factory.as_deref()).await {
match connect_ws(
&self.config,
rdcleanpath,
self.cliprdr_factory.as_deref(),
&self.dvc_pipe_proxy_factory,
)
.await
{
Ok(result) => result,
Err(e) => {
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e));
break;
}
}
} else {
match connect(&self.config, self.cliprdr_factory.as_deref()).await {
match connect(
&self.config,
self.cliprdr_factory.as_deref(),
&self.dvc_pipe_proxy_factory,
)
.await
{
Ok(result) => result,
Err(e) => {
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e));
Expand Down Expand Up @@ -118,6 +163,7 @@ type UpgradedFramed = ironrdp_tokio::TokioFramed<Box<dyn AsyncReadWrite + Unpin
async fn connect(
config: &Config,
cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>,
dvc_pipe_proxy_factory: &DvcPipeProxyFactory,
) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> {
let dest = format!("{}:{}", config.destination.name(), config.destination.port());

Expand All @@ -131,10 +177,21 @@ async fn connect(

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

let mut drdynvc =
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));

// Instantiate all DVC proxies
for proxy in config.dvc_pipe_proxies.iter() {
let channel_name = proxy.channel_name.clone();
let pipe_name = proxy.pipe_name.clone();

trace!(%channel_name, %pipe_name, "Creating DVC proxy");

drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name));
}

let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr)
.with_static_channel(
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))),
)
.with_static_channel(drdynvc)
.with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new())))
.with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0));

Expand Down Expand Up @@ -182,6 +239,7 @@ async fn connect_ws(
config: &Config,
rdcleanpath: &RDCleanPathConfig,
cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>,
dvc_pipe_proxy_factory: &DvcPipeProxyFactory,
) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> {
let hostname = rdcleanpath
.url
Expand Down Expand Up @@ -210,10 +268,21 @@ async fn connect_ws(

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

let mut drdynvc =
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));

// Instantiate all DVC proxies
for proxy in config.dvc_pipe_proxies.iter() {
let channel_name = proxy.channel_name.clone();
let pipe_name = proxy.pipe_name.clone();

trace!(%channel_name, %pipe_name, "Creating DVC proxy");

drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name));
}

let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr)
.with_static_channel(
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))),
)
.with_static_channel(drdynvc)
.with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new())))
.with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0));

Expand Down Expand Up @@ -464,6 +533,12 @@ async fn active_session(
Vec::new()
}
}
RdpInputEvent::SendDvcMessages { channel_id, messages } => {
trace!(channel_id, ?messages, "Send DVC messages");

let frame = active_stage.encode_dvc_messages(messages)?;
vec![ActiveStageOutput::ResponseFrame(frame)]
}
}
}
};
Expand Down
6 changes: 6 additions & 0 deletions crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
39 changes: 39 additions & 0 deletions crates/ironrdp-dvc-pipe-proxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "ironrdp-dvc-pipe-proxy"
version = "0.1.0"
readme = "README.md"
description = "DVC named pipe proxy for IronRDP"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true

[lib]
doctest = false
test = false

[dependencies]
ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.2" } # public
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.4" } # public
ironrdp-svc = { path = "../ironrdp-svc", version = "0.3" } # public

tracing = { version = "0.1", features = ["log"] }


[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
"Win32_System_Pipes",
"Win32_System_IO",
] }


[lints]
workspace = true
1 change: 1 addition & 0 deletions crates/ironrdp-dvc-pipe-proxy/LICENSE-APACHE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../LICENSE-APACHE
1 change: 1 addition & 0 deletions crates/ironrdp-dvc-pipe-proxy/LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../LICENSE-MIT
5 changes: 5 additions & 0 deletions crates/ironrdp-dvc-pipe-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# IronRDP DVC pipe proxy

Generic DVC handler which makes IronRDP connect to specific DVC channel and create a named pipe
server, which will be used for proxying DVC messages to/from user-defined DVC logic
implemented as named pipe clients (either in the same process or in a different process).
12 changes: 12 additions & 0 deletions crates/ironrdp-dvc-pipe-proxy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]

#[macro_use]
extern crate tracing;

#[cfg(target_os = "windows")]
mod windows;

mod platform;

pub use platform::*;
5 changes: 5 additions & 0 deletions crates/ironrdp-dvc-pipe-proxy/src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(target_os = "windows")]
mod windows;

#[cfg(target_os = "windows")]
pub use windows::DvcNamedPipeProxy;
Loading
Loading