Skip to content

feat(server): make run_connection generic over stream type#1181

Merged
Benoît Cortier (CBenoit) merged 3 commits into
Devolutions:masterfrom
lamco-admin:feat/generic-run-connection
Mar 23, 2026
Merged

feat(server): make run_connection generic over stream type#1181
Benoît Cortier (CBenoit) merged 3 commits into
Devolutions:masterfrom
lamco-admin:feat/generic-run-connection

Conversation

@glamberson
Copy link
Copy Markdown
Contributor

Summary

Make run_connection accept any AsyncRead + AsyncWrite stream instead of requiring TcpStream concretely.

  • run_connection<S>(&mut self, stream: S) where S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static
  • Replace peer_addr() call in Hybrid/CredSSP path with local_addr fallback
  • Remove unused TcpStream import

Motivation

RDP servers may accept connections from transports other than TCP: Unix domain sockets (for local WebSocket relay), VSOCK (VM-to-host), or in-process streams (integration tests). The current TcpStream parameter prevents this, even though everything downstream of run_connection already operates generically (TokioFramed<S>, TLS accept, accept_finalize<S>, client_loop<R, W>).

Concrete use case: a QEMU display server that listens on both TCP (native RDP clients) and a Unix domain socket (browser clients via WebSocket relay) using the same RdpServer instance.

Changes

1 file, +8/-5 lines:

  • Generalize run_connection signature with AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static bounds (matching accept_finalize)
  • Replace peer_addr() in the Hybrid/CredSSP path: the existing comment noted "doesn't seem to matter yet" for NTLM auth. Use local_addr as fallback since generic streams don't expose peer address
  • Remove now-unused TcpStream import (TcpListener remains for run())

Backward Compatibility

Fully backward compatible. Callers passing TcpStream continue to work unchanged via type inference. run() is unaffected.

Test Plan

  • cargo xtask check fmt -v passes
  • cargo xtask check lints -v passes
  • cargo xtask check tests -v passes
  • cargo xtask check typos -v passes
  • Verified run() compiles with inferred TcpStream
  • Verified UnixStream accepted via run_connection in downstream project

(Replaces #1180, which was closed due to head branch move.)

Make `run_connection` accept any `AsyncRead + AsyncWrite` stream instead
of requiring `TcpStream` concretely. This enables RDP servers to accept
connections from Unix domain sockets, VSOCK, in-process test streams, or
any other bidirectional byte stream.

Everything inside `run_connection` already operated generically:
`TokioFramed<S>`, TLS accept, and `accept_finalize` all use trait bounds
rather than concrete types. The only `TcpStream`-specific call was
`peer_addr()` in the CredSSP/Hybrid path, which the existing comment
noted "doesn't seem to matter yet" for NTLM auth. Replace it with
`local_addr` as a fallback client name.

Backward compatible: callers passing `TcpStream` continue to work
unchanged via type inference. `run()` is unaffected.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR generalizes RdpServer::run_connection to accept arbitrary Tokio AsyncRead + AsyncWrite streams instead of a concrete TcpStream, enabling non-TCP transports (e.g., Unix sockets, VSOCK, in-process streams) to reuse the same server connection logic.

Changes:

  • Make run_connection generic over a stream type implementing AsyncRead + AsyncWrite (and related bounds).
  • Replace the CredSSP “client name” derivation (previously from peer_addr()) with a local_addr-based fallback.
  • Remove the now-unused TcpStream import.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ironrdp-server/src/server.rs Outdated
Comment thread crates/ironrdp-server/src/server.rs Outdated
Comment thread crates/ironrdp-server/src/server.rs Outdated
- Drop unnecessary 'static bound (accept_finalize doesn't require it)
- Use constant "rdp-client" fallback for CredSSP client name instead
  of self.local_addr (which is the server address, not the peer)
- Add comment explaining the placeholder and its context
Comment thread crates/ironrdp-server/src/server.rs Outdated
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@CBenoit Benoît Cortier (CBenoit) enabled auto-merge (squash) March 23, 2026 22:52
@CBenoit Benoît Cortier (CBenoit) merged commit c30d853 into Devolutions:master Mar 23, 2026
10 checks passed
@glamberson Greg Lamberson (glamberson) deleted the feat/generic-run-connection branch March 23, 2026 23:34
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request May 20, 2026
Reshape the public surface introduced earlier in this PR before it
lands, to avoid a future breaking touch when ironrdp-server's
anyhow-removal pass arrives. Mirrors the post-Devolutions#1264 hand-rolled
error pattern (Display + core::error::Error impls, no thiserror).

Trait signature was `Result<bool>` (i.e. anyhow::Result through the
file-level `use anyhow::Result`). Two issues:

- New public surface depending on anyhow at a moment when the
  workspace is moving the other way (Devolutions#1277 just removed anyhow from
  rdpsnd-native).
- `Ok(true)` vs `Ok(false)` is stringly typed at the call site: a
  bare bool with no semantic clue.

New shape:

- `CredentialDecision::{Accept, Reject}` for the binary outcome.
- `CredentialValidationError` wrapping any `core::error::Error +
  Send + Sync` for the case where the validator backend itself
  failed (LDAP unreachable, PAM transport error, etc.). Manual
  Display + Error impls; source chains through.
- `validate(&self, &Credentials) -> Result<CredentialDecision,
  CredentialValidationError>`.

The accept/reject/backend-error trichotomy is now explicit at every
call site. Rejection is no longer an error; backend failure is.
Log + bail strings updated to match the trichotomy.

Also addresses the API consistency note: every other configurable
on `RdpServerBuilder<BuilderDone>` goes through `with_*` on the
builder. Added `with_credential_validator(Option<Arc<dyn ...>>)`
following the same shape as `with_connection_handler`. The
post-construction `set_credential_validator` setter is kept (now
takes `Option` to match the field and the builder's setter shape)
for dynamic reconfiguration after `build()`.

Tests added in `server.rs` cover Accept / Reject / backend-error
propagation and `Arc<dyn CredentialValidator>` Send + Sync + 'static
bounds. Integration with `client_accepted` is exercised through
the existing acceptor-side tests once the validator is wired via
the builder; no new integration test in this commit (server-side
precedent per Devolutions#1181 / Devolutions#1187 / Devolutions#1281).

Verification: `cargo test -p ironrdp-server --lib` (14 passed),
`cargo clippy -p ironrdp-server --all-targets -- -D warnings` clean,
`cargo check -p ironrdp-server` clean.

Refs Devolutions#1154, Devolutions#1150, Devolutions#1155.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request May 21, 2026
…ypted streams

Adds a sibling to RdpServer::run_connection that walks the same
per-connection state machine but skips the IronRDP-managed TLS handshake.
The caller's stream must already be transport-encrypted at a lower layer
(typically a WebSocket Secure terminator in an RDCleanPath-shaped
deployment).

The implementation mirrors run_connection except for one step: on
BeginResult::ShouldUpgrade, instead of calling tls_acceptor.accept(stream),
the new method calls Acceptor::mark_security_upgrade_as_done() to advance
the state machine and re-wraps the inner stream as already-post-TLS. The
Hybrid CredSSP block, accept_finalize, and shutdown sequence are
identical to run_connection because CredSSP carries its own crypto via
TSRequest and does not require the underlying transport's TLS.

Builds on PR Devolutions#1181 which made run_connection generic over any
AsyncRead+AsyncWrite stream. This method extends the same design intent
to streams that have been TLS-terminated by a lower layer.

Wire-level invariant preserved: the X.224 negotiation is untouched. The
acceptor still advertises whatever SecurityProtocol it was constructed
with; only the TLS-handshake step is skipped. Earlier attempts at a
wire-level signal (PR Devolutions#1210, RdpServerSecurity::PreSecured) failed
interop with vanilla clients and were closed; this method sidesteps
that approach by relying on a higher-layer protocol (RDCleanPath) to
inform the client that TLS happened elsewhere.

Considered and rejected: a new RdpServerSecurity::PreAuthenticated
variant. The canonical deployment serves both vanilla TCP+TLS clients
and WSS+RDCleanPath clients from a single server instance on different
listeners; per-connection choice fits that use case, while a variant
would force splitting into two server instances and break exhaustive
matches downstream. Sibling method has zero API breakage.

A NOTE comment in the source records the synchronization requirement
with run_connection's ShouldUpgrade arm so future rebases catch
upstream divergence.

The motivating downstream consumer is lamco-rdp-server's WebSocket plus
RDCleanPath listener, which retires its external ws-rdp-proxy from the
production WASM-client path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants