Skip to content
Merged
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
5 changes: 5 additions & 0 deletions Cargo.lock

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

16 changes: 15 additions & 1 deletion crates/ironrdp-acceptor/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ pub struct Acceptor {
/// Source of randomness for the Initiate Multitransport Request's security
/// cookie and request ID. See `set_multitransport_security_rng()`.
multitransport_security_rng: Box<dyn MultitransportSecurityRng>,
/// Whether the Initiate Multitransport Response matching
/// `sent_multitransport_request` was received, and whether it reported
/// success. `None` until a matching response arrives. See
/// [`AcceptorResult::multitransport_response_success`].
received_multitransport_response: Option<bool>,
}

/// Source of randomness for the security cookie and request ID the acceptor
Expand Down Expand Up @@ -185,6 +190,11 @@ pub struct AcceptorResult {
/// implement UDP multitransport can use it to decide whether to send a
/// Server Initiate Multitransport Request.
pub multitransport_flags: gcc::MultiTransportFlags,
/// Whether the Initiate Multitransport Response matching the sent
/// request was received during the connection sequence, and whether it
/// reported success. `None` when no request was sent, or a matching
/// response never arrived.
pub multitransport_response_success: Option<bool>,
/// Credentials received from the client during SecureSettingsExchange.
///
/// Present for TLS-mode connections where the client sends credentials
Expand Down Expand Up @@ -234,6 +244,7 @@ impl Acceptor {
advertised_multitransport: None,
sent_multitransport_request: None,
multitransport_security_rng: Box::new(OsMultitransportSecurityRng),
received_multitransport_response: None,
}
}

Expand Down Expand Up @@ -386,7 +397,7 @@ impl Acceptor {
/// handling for anything that isn't really a response, mirroring how
/// `ClientConnectorState::ConnectTimeAutoDetection` demuxes the same
/// channel client-side.
fn is_late_multitransport_response(&self, data: &mcs::SendDataRequest<'_>) -> bool {
fn is_late_multitransport_response(&mut self, data: &mcs::SendDataRequest<'_>) -> bool {
let Some(sent) = self.sent_multitransport_request.as_ref() else {
return false;
};
Expand All @@ -411,6 +422,7 @@ impl Acceptor {
success = response.is_success(),
"Received Initiate Multitransport Response"
);
self.received_multitransport_response = Some(response.is_success());
} else {
warn!(
response.request_id,
Expand Down Expand Up @@ -467,6 +479,7 @@ impl Acceptor {
advertised_multitransport: consumed.advertised_multitransport,
sent_multitransport_request: consumed.sent_multitransport_request,
multitransport_security_rng: consumed.multitransport_security_rng,
received_multitransport_response: consumed.received_multitransport_response,
})
}

Expand Down Expand Up @@ -560,6 +573,7 @@ impl Acceptor {
multitransport_flags: self
.multitransport_flags
.unwrap_or_else(gcc::MultiTransportFlags::empty),
multitransport_response_success: self.received_multitransport_response,
client_early_capability_flags: self.early_capability_flags,
reactivation: self.reactivation,
credentials: self.received_credentials.take(),
Expand Down
66 changes: 65 additions & 1 deletion crates/ironrdp-dvc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ impl DrdynvcServer {
/// This API emits exactly one `ReliableUdp` channel list and maps every supplied
/// channel to that list. A future multi-tunnel request API must establish an
/// explicit response-routing mapping before it is exposed.
///
/// A connection gets one request: The state never returns to idle, so a
/// later call returns an error. The request declares the TCP path flushed for
/// the supplied channels (SOFT_SYNC_TCP_FLUSHED, [MS-RDPEDYC] 2.2.5.1), so the
/// server sends their data over the tunnel from then on ([MS-RDPEDYC]
/// 3.3.5.3.1), whatever the client's response lists.
pub fn request_reliable_udp(&mut self, channel_ids: Vec<u32>) -> PduResult<SvcMessage> {
if channel_ids.is_empty() {
return Err(pdu_other_err!("soft-sync requires at least one dynamic channel"));
Expand Down Expand Up @@ -352,6 +358,19 @@ impl DrdynvcServer {
self.outgoing_tunnel_channels.get(&channel_id).copied()
}

/// Returns whether a Soft-Sync request was sent and its response has not
/// arrived yet, the window in which the server must not read tunnel data
/// (MS-RDPEDYC 3.3.5.3.2).
pub const fn soft_sync_awaiting_response(&self) -> bool {
matches!(
self.soft_sync_state,
SoftSyncState::Active {
response_received: false,
..
}
)
}

/// Returns whether the client has acknowledged the Soft-Sync request over TCP.
pub const fn soft_sync_response_received(&self) -> bool {
matches!(
Expand Down Expand Up @@ -408,12 +427,24 @@ impl DrdynvcServer {
return Err(pdu_other_err!("soft-sync response selected an unrequested tunnel"));
}
}
self.incoming_tunnel_channels = self
let accepted_channels: BTreeMap<u32, SoftSyncTunnelType> = self
.outgoing_tunnel_channels
.iter()
.filter(|(_, tunnel_type)| response.tunnels_to_switch().contains(tunnel_type))
.map(|(channel_id, tunnel_type)| (*channel_id, *tunnel_type))
.collect();
// The response names the tunnels the client will write on
// (MS-RDPEDYC 2.2.5.2), so it sets only what the server reads from a
// tunnel. The server's own sending was fixed by the request: after
// sending it, the server MUST keep using the tunnel it named for
// those channels (3.3.5.3.1), and the request told the client that no
// more of their data comes over TCP.
debug!(
tunnels = ?response.tunnels_to_switch(),
channels = ?accepted_channels.keys().collect::<Vec<_>>(),
"Soft-Sync response received"
);
self.incoming_tunnel_channels = accepted_channels;
*response_received = true;
Ok(())
}
Expand Down Expand Up @@ -562,6 +593,39 @@ mod tests {
assert!(server.request_reliable_udp(alloc::vec![channel_id]).is_err());
}

#[test]
fn soft_sync_response_does_not_change_the_outgoing_tunnel() {
let mut server = DrdynvcServer::new();
let channel_id = server.dynamic_channels.insert_channel(TestDvc, ChannelState::Opened);

server.request_reliable_udp(alloc::vec![channel_id]).unwrap();
assert_eq!(
server.tunnel_for_outgoing_channel(channel_id),
Some(SoftSyncTunnelType::RELIABLE_UDP)
);

// An empty list means the client keeps writing over TCP. It does not
// take back the server's own switch, which the request announced.
server
.process_soft_sync_response(crate::pdu::SoftSyncResponsePdu::new(alloc::vec![]))
.unwrap();

assert!(server.soft_sync_response_received());
assert_eq!(
server.tunnel_for_outgoing_channel(channel_id),
Some(SoftSyncTunnelType::RELIABLE_UDP),
"the server keeps sending on the tunnel its request named"
);
let tunnel_data = ironrdp_core::encode_vec(&DrdynvcClientPdu::Data(crate::pdu::DrdynvcDataPdu::Data(
crate::pdu::DataPdu::new(channel_id, Vec::new()),
)))
.unwrap();
assert!(
server.process_tunnel(&tunnel_data).is_err(),
"the client did not switch its own writing, so tunnel data for the channel is unexpected"
);
}

#[test]
fn soft_sync_accepts_a_response_after_the_selected_channel_closes() {
let mut server = DrdynvcServer::new();
Expand Down
4 changes: 3 additions & 1 deletion crates/ironrdp-rdpeudp-tokio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ pub(crate) mod tunnel;

pub use self::error::{DriverError, DriverErrorKind, UdpTransportError, UdpTransportErrorKind};
pub use self::multitransport::MultitransportBootstrap;
pub use self::transport::{UdpAcceptConfig, UdpTlsConfig, UdpTransport, UdpTransportConfig, accept_udp, connect_udp};
pub use self::transport::{
UdpAcceptConfig, UdpTlsConfig, UdpTransport, UdpTransportConfig, UdpTransportSender, accept_udp, connect_udp,
};
54 changes: 46 additions & 8 deletions crates/ironrdp-rdpeudp-tokio/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,38 @@ impl<T> Drop for AbortOnDrop<T> {
}
}

/// A cloneable handle for sending data over an established UDP transport,
/// obtained from [`UdpTransport::sender`].
///
/// Independent of [`UdpTransport::recv`]'s `&mut self` requirement: Sending
/// and receiving already run over separate channels fed by separate
/// background tasks, so this never contends with a concurrent `recv()`.
#[derive(Clone)]
pub struct UdpTransportSender(mpsc::Sender<Vec<u8>>);

impl UdpTransportSender {
/// Send a higher-layer data frame through the tunnel.
///
/// Identical validation and error semantics to [`UdpTransport::send`],
/// which delegates to this.
///
/// # Errors
///
/// Returns `PayloadTooLarge` if `data` exceeds 65535 bytes, the wire
/// `PayloadLength` field's capacity ([MS-RDPEMT] 2.2.2.3).
pub async fn send(&self, data: Vec<u8>) -> Result<(), UdpTransportError> {
if data.len() > usize::from(u16::MAX) {
debug!(len = data.len(), "Rejected oversized tunnel payload");
return Err(UdpTransportError::payload_too_large("send", data.len()));
}

self.0
.send(data)
.await
.map_err(|_| UdpTransportError::driver("send", DriverError::connection_closed("send")))
}
}

/// Handle to an established UDP transport.
///
/// Provides bidirectional higher-layer data (DVC frames) over the
Expand Down Expand Up @@ -261,15 +293,21 @@ impl UdpTransport {
/// discover: that task has no way to report a per-payload failure back
/// to a caller who already received `Ok(())` from a channel send.
pub async fn send(&self, data: Vec<u8>) -> Result<(), UdpTransportError> {
if data.len() > usize::from(u16::MAX) {
debug!(len = data.len(), "Rejected oversized tunnel payload");
return Err(UdpTransportError::payload_too_large("send", data.len()));
}
self.sender().send(data).await
}

self.data_tx
.send(data)
.await
.map_err(|_| UdpTransportError::driver("send", DriverError::connection_closed("send")))
/// Returns a cloneable handle for sending data, independent of this
/// object's `&mut self`-requiring [`Self::recv`].
///
/// Sending and receiving are already independent internally (separate
/// channels fed by separate background tasks), so a caller that shares
/// one `UdpTransport` between a single dedicated receiver (behind a lock
/// reserved for `recv()` alone, since only one caller should ever call
/// it) and one or more senders can send through this handle without
/// contending on that lock, including for the full duration of an idle
/// `recv()` wait.
pub fn sender(&self) -> UdpTransportSender {
UdpTransportSender(self.data_tx.clone())
}

/// Shut down the transport, closing the RDPEUDP2 connection.
Expand Down
3 changes: 3 additions & 0 deletions crates/ironrdp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" } # public
ironrdp-rdpeai = { path = "../ironrdp-rdpeai", version = "0.1" } # public
ironrdp-rdpeusb = { path = "../ironrdp-rdpeusb", version = "0.1", optional = true }
ironrdp-usb = { path = "../ironrdp-usb", version = "0.1", optional = true }
ironrdp-rdpemt = { path = "../ironrdp-rdpemt", version = "0.1" } # public
ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", version = "0.1" } # public
ironrdp-rdpeudp-tokio = { path = "../ironrdp-rdpeudp-tokio", version = "0.1" } # public
Comment thread
glamberson marked this conversation as resolved.
tracing = { version = "0.1", features = ["log"] }
x509-cert = { version = "0.3", optional = true }
rustls-pemfile = { version = "2.2", optional = true }
Expand Down
36 changes: 36 additions & 0 deletions crates/ironrdp-server/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub struct BuilderDone {
connection_policy: ConnectionPolicy,
remotefx_quant: Quant,
remotefx_entropy_coder: Option<EntropyBits>,
udp_bind_addr: Option<SocketAddr>,
}

pub struct RdpServerBuilder<State> {
Expand Down Expand Up @@ -180,6 +181,7 @@ impl RdpServerBuilder<WantsDisplay> {
auto_reconnect_cookie: None,
remotefx_quant: Quant::default(),
remotefx_entropy_coder: None,
udp_bind_addr: None,
},
}
}
Expand Down Expand Up @@ -215,6 +217,7 @@ impl RdpServerBuilder<WantsDisplay> {
auto_reconnect_cookie: None,
remotefx_quant: Quant::default(),
remotefx_entropy_coder: None,
udp_bind_addr: None,
},
}
}
Expand Down Expand Up @@ -479,6 +482,38 @@ impl RdpServerBuilder<BuilderDone> {
self
}

/// Offer UDP multitransport (MS-RDPBCGR 2.2.1.4.6/2.2.15.1) to clients
/// that support it, binding a fresh UDP socket to `udp_bind_addr` per
/// connection to accept the sideband RDPEUDP2 + TLS + RDPEMT transport.
///
/// Requires [`RdpServerSecurity::Tls`] or [`RdpServerSecurity::Hybrid`]
/// (multitransport is Enhanced-Security-only, matching the reference
/// client); ignored under [`RdpServerSecurity::None`].
///
/// `udp_bind_addr` is a separate, explicit address rather than reusing
/// [`Self`]'s own TCP `addr`: a caller driving [`RdpServer::run_connection`]
/// with its own accept loop (rather than [`RdpServer::run`]) may not have
/// `addr` bound to anything real, so it cannot be inferred. Typically the
/// same host and port as the TCP listener (UDP and TCP occupy independent
/// port spaces at the same number). When its IP is unspecified, each
/// connection's socket binds to the local address that client reached
/// instead, so replies leave from the address the client sent to; see
/// [`RdpServer::set_connection_local_addr`].
///
/// Once established, the transport is used to migrate EGFX graphics
/// traffic off TCP; a failure to establish it at any stage falls back to
/// TCP-only rather than failing the connection. This includes
/// [`Self::with_preempt_existing_session`] overlapping a candidate
/// session's own UDP bind with a still-live session's: When
/// `udp_bind_addr` is the same for both, the second bind fails and that
/// connection degrades to TCP-only.
///
/// `None` (the default): no UDP socket is ever bound, no behavior change.
pub fn with_udp_transport(mut self, udp_bind_addr: SocketAddr) -> Self {
self.state.udp_bind_addr = Some(udp_bind_addr);
self
}
Comment thread
glamberson marked this conversation as resolved.

pub fn build(self) -> RdpServer {
let mut server = RdpServer::new(
RdpServerOptions {
Expand All @@ -490,6 +525,7 @@ impl RdpServerBuilder<BuilderDone> {
connection_policy: self.state.connection_policy,
remotefx_quant: self.state.remotefx_quant,
remotefx_entropy_coder: self.state.remotefx_entropy_coder,
udp_bind_addr: self.state.udp_bind_addr,
},
self.state.handler,
self.state.display,
Expand Down
36 changes: 36 additions & 0 deletions crates/ironrdp-server/src/gfx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,39 @@ impl core::fmt::Display for EgfxServerMessage {
}
}
}

/// The dynamic channel id EGFX is registered under: the bridge when the
/// factory handed out a frame handle, otherwise the server itself.
pub(crate) fn egfx_channel_id(drdynvc: &ironrdp_dvc::DrdynvcServer) -> Option<u32> {
drdynvc
.get_channel_id_by_type::<GfxDvcBridge>()
.or_else(|| drdynvc.get_channel_id_by_type::<GraphicsPipelineServer>())
}

#[cfg(test)]
mod tests {
use ironrdp_dvc::DrdynvcServer;
use ironrdp_egfx::pdu::{CapabilitiesAdvertisePdu, CapabilitySet};

use super::*;

struct Handler;

impl GraphicsPipelineHandler for Handler {
fn capabilities_advertise(&mut self, _pdu: &CapabilitiesAdvertisePdu) {}

fn on_ready(&mut self, _negotiated: &CapabilitySet) {}
}

#[test]
fn egfx_is_found_whichever_way_it_was_registered() {
let server = Arc::new(Mutex::new(GraphicsPipelineServer::new(Box::new(Handler))));
let bridged = DrdynvcServer::new().with_dynamic_channel(GfxDvcBridge::new(server));
assert!(egfx_channel_id(&bridged).is_some());

let direct = DrdynvcServer::new().with_dynamic_channel(GraphicsPipelineServer::new(Box::new(Handler)));
assert!(egfx_channel_id(&direct).is_some());

assert!(egfx_channel_id(&DrdynvcServer::new()).is_none());
}
}
1 change: 1 addition & 0 deletions crates/ironrdp-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod handler;
pub mod heartbeat;
#[cfg(feature = "helper")]
mod helper;
mod multitransport;
mod rdpdr;
mod rdpeai;
mod rdpei;
Expand Down
Loading
Loading