From c111230dda3a09555fa3acfee36a5f60f0f983cc Mon Sep 17 00:00:00 2001 From: bernie-g Date: Sun, 10 May 2026 20:50:06 -0400 Subject: [PATCH 01/16] feat(pam): add browser-based RDP session support and recording Add RDCleanPath protocol support, browser RDP session handling via Gateway, and session recording capture for Windows RDP connections. --- packages/gateway-v2/gateway.go | 12 +- .../pam/handlers/rdp/bridge_cgo_shared.go | 56 ++-- packages/pam/handlers/rdp/bridge_cgo_unix.go | 67 ++-- .../pam/handlers/rdp/bridge_cgo_windows.go | 59 +++- packages/pam/handlers/rdp/bridge_stub.go | 13 + packages/pam/handlers/rdp/native/Cargo.lock | 10 + packages/pam/handlers/rdp/native/Cargo.toml | 1 + .../handlers/rdp/native/include/rdp_bridge.h | 31 ++ .../pam/handlers/rdp/native/src/bridge.rs | 16 +- packages/pam/handlers/rdp/native/src/caps.rs | 98 ++++++ .../pam/handlers/rdp/native/src/config.rs | 41 ++- packages/pam/handlers/rdp/native/src/ffi.rs | 177 +++++++++- packages/pam/handlers/rdp/native/src/lib.rs | 2 + .../handlers/rdp/native/src/rdcleanpath.rs | 309 ++++++++++++++++++ packages/pam/handlers/rdp/proxy.go | 11 +- packages/pam/local/database-proxy.go | 1 + packages/pam/local/rdp-proxy.go | 22 +- packages/pam/pam-proxy.go | 9 +- 18 files changed, 841 insertions(+), 94 deletions(-) create mode 100644 packages/pam/handlers/rdp/native/src/caps.rs create mode 100644 packages/pam/handlers/rdp/native/src/rdcleanpath.rs diff --git a/packages/gateway-v2/gateway.go b/packages/gateway-v2/gateway.go index 07c32120..acb288b2 100644 --- a/packages/gateway-v2/gateway.go +++ b/packages/gateway-v2/gateway.go @@ -34,6 +34,7 @@ const ( ForwardModeHTTP ForwardMode = "HTTP" ForwardModeTCP ForwardMode = "TCP" ForwardModePAM ForwardMode = "PAM" + ForwardModePAMRDPBrowser ForwardMode = "PAM_RDP_BROWSER" ForwardModePAMCancellation ForwardMode = "PAM_CANCELLATION" ForwardModePAMCapabilities ForwardMode = "PAM_CAPABILITIES" ForwardModePing ForwardMode = "PING" @@ -586,7 +587,7 @@ func (g *Gateway) setupTLSConfig() error { ClientCAs: clientCAPool, ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS12, - NextProtos: []string{"infisical-http-proxy", "infisical-tcp-proxy", "infisical-ping", "infisical-pam-proxy", "infisical-pam-session-cancellation", "infisical-pam-capabilities"}, + NextProtos: []string{"infisical-http-proxy", "infisical-tcp-proxy", "infisical-ping", "infisical-pam-proxy", "infisical-pam-rdp-browser", "infisical-pam-session-cancellation", "infisical-pam-capabilities"}, } return nil @@ -749,11 +750,12 @@ func (g *Gateway) handleIncomingChannel(newChannel ssh.NewChannel) { log.Info().Msg("TCP proxy handler completed") } return - } else if forwardConfig.Mode == ForwardModePAM { + } else if forwardConfig.Mode == ForwardModePAM || forwardConfig.Mode == ForwardModePAMRDPBrowser { sessionCtx, sessionCancel := context.WithCancel(g.ctx) g.RegisterPAMSession(forwardConfig.PAMConfig.SessionId, sessionCancel, tlsConn) defer g.DeregisterPAMSession(forwardConfig.PAMConfig.SessionId, tlsConn) - if err := pam.HandlePAMProxy(sessionCtx, tlsConn, &forwardConfig.PAMConfig, g.httpClient); err != nil { + browserRDP := forwardConfig.Mode == ForwardModePAMRDPBrowser + if err := pam.HandlePAMProxy(sessionCtx, tlsConn, &forwardConfig.PAMConfig, g.httpClient, browserRDP); err != nil { if err.Error() == "unexpected EOF" { log.Debug().Err(err).Msg("PAM proxy handler ended with unexpected connection termination") } else { @@ -816,6 +818,10 @@ func (g *Gateway) parseForwardConfigFromALPN(tlsConn *tls.Conn, reader *bufio.Re config.Mode = ForwardModePAM return config, nil + case "infisical-pam-rdp-browser": + config.Mode = ForwardModePAMRDPBrowser + return config, nil + case "infisical-pam-session-cancellation": config.Mode = ForwardModePAMCancellation return config, nil diff --git a/packages/pam/handlers/rdp/bridge_cgo_shared.go b/packages/pam/handlers/rdp/bridge_cgo_shared.go index e6d8c7d1..b2e155f1 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_shared.go +++ b/packages/pam/handlers/rdp/bridge_cgo_shared.go @@ -20,6 +20,34 @@ import ( ) func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { + return p.handleConnectionWith(ctx, clientConn, func() (*Bridge, error) { + return StartWithReadWriter( + clientConn, + p.config.TargetHost, + p.config.TargetPort, + p.config.InjectUsername, + p.config.InjectPassword, + p.config.InjectDomain, + ) + }) +} + +// HandleConnectionRDCleanPath is the browser-flow variant (RDCleanPath instead of X.224). +func (p *RDPProxy) HandleConnectionRDCleanPath(ctx context.Context, clientConn net.Conn) error { + return p.handleConnectionWith(ctx, clientConn, func() (*Bridge, error) { + return StartRDCleanPathWithReadWriter( + clientConn, + p.config.TargetHost, + p.config.TargetPort, + p.config.InjectUsername, + p.config.InjectPassword, + p.config.InjectDomain, + BrowserAcceptorUsername, + ) + }) +} + +func (p *RDPProxy) handleConnectionWith(ctx context.Context, clientConn net.Conn, start func() (*Bridge, error)) error { defer clientConn.Close() if p.config.SessionLogger != nil { defer func() { @@ -27,38 +55,24 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er }() } - bridge, err := StartWithReadWriter( - clientConn, - p.config.TargetHost, - p.config.TargetPort, - p.config.InjectUsername, - p.config.InjectPassword, - p.config.InjectDomain, - ) + bridge, err := start() if err != nil { return fmt.Errorf("rdp proxy: start bridge: %w", err) } defer bridge.Close() - // Drain bridge tap events into the session logger. The Rust side closes - // the events channel when the session ends, so the goroutine exits via - // PollEnded without needing an explicit shutdown signal. drainCtx, cancelDrain := context.WithCancel(ctx) drainDone := make(chan struct{}) go func() { defer close(drainDone) drainBridgeEvents(drainCtx, bridge, p.config.SessionLogger, p.config.SessionID, p.config.SessionStartedAt) }() - // Wait for the drain to finish naturally on the normal-end path so the - // tail of the recording isn't dropped: PollEnded fires after the Rust - // side closes the events channel (post bridge.Wait return). Cancellation - // paths trigger cancelDrain() explicitly below to bail early. + // Let drain finish so recording tail isn't dropped; cancel paths bail early defer func() { select { case <-drainDone: case <-time.After(2 * pollTimeout): } - // Always release the drain context (no-op if already cancelled). cancelDrain() }() @@ -95,8 +109,7 @@ func (b *Bridge) Wait() error { } } -// Cancel is idempotent and safe from any goroutine, including -// concurrently with Wait. +// Cancel is idempotent and safe from any goroutine. func (b *Bridge) Cancel() error { rc := C.rdp_bridge_cancel(C.uint64_t(b.handle)) if rc == C.RDP_BRIDGE_INVALID_HANDLE { @@ -120,9 +133,7 @@ func (b *Bridge) Close() error { // True when the real bridge is compiled in (vs the stub). func IsSupported() bool { return true } -// PollEvent drains one tap event with the given timeout. The returned Event -// is only meaningful when result == PollOK. PollEvent is not safe to call -// concurrently for the same Bridge; serialize calls in a single goroutine. +// PollEvent drains one tap event. Not safe to call concurrently for the same Bridge. func (b *Bridge) PollEvent(timeout time.Duration) (PollResult, Event, error) { timeoutMs := timeout.Milliseconds() if timeoutMs < 0 { @@ -164,8 +175,7 @@ func (b *Bridge) PollEvent(timeout time.Duration) (PollResult, Event, error) { ev.X = uint16(raw.value_a) ev.Y = uint16(raw.value_b) case EventTypeTargetFrame: - // Always free the libc-malloc'd buffer Rust handed us, even if - // the copy below is empty -- ownership transfer is unconditional. + // Ownership transferred from Rust; always free even if empty if raw.payload_ptr != nil { defer C.free(unsafe.Pointer(raw.payload_ptr)) if raw.payload_len > 0 { diff --git a/packages/pam/handlers/rdp/bridge_cgo_unix.go b/packages/pam/handlers/rdp/bridge_cgo_unix.go index f5d3f454..b074faf1 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_unix.go +++ b/packages/pam/handlers/rdp/bridge_cgo_unix.go @@ -21,19 +21,25 @@ import ( ) // StartWithConn hands an independent dup of conn's fd to the bridge. -// For TLS-wrapped or otherwise non-fd-backed conns, use StartWithReadWriter. -// `domain` is empty for local accounts; set to the AD domain name for -// domain-joined NTLM CredSSP. func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) { dupFd, err := dupConnFD(conn) if err != nil { return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err) } - return startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain) + return startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain, "") } -// Ownership of dupFd transfers to Rust on success; we close it on failure. -func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) { +// StartRDCleanPathWithConn is the browser-flow analog of StartWithConn. +func StartRDCleanPathWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { + dupFd, err := dupConnFD(conn) + if err != nil { + return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err) + } + return startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain, acceptorUsername) +} + +// Ownership of dupFd transfers to Rust on success; closed on failure. +func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { success := false defer func() { if !success { @@ -48,7 +54,6 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, cPass := C.CString(password) defer C.free(unsafe.Pointer(cPass)) - // Empty domain -> NULL pointer; bridge treats both the same way. var cDomain *C.char if domain != "" { cDomain = C.CString(domain) @@ -56,15 +61,31 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, } var handle C.uint64_t - rc := C.rdp_bridge_start_unix_fd( - C.int(dupFd), - cHost, - C.uint16_t(targetPort), - cUser, - cPass, - cDomain, - &handle, - ) + var rc C.int32_t + if acceptorUsername == "" { + rc = C.rdp_bridge_start_unix_fd( + C.int(dupFd), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + cDomain, + &handle, + ) + } else { + cAcceptor := C.CString(acceptorUsername) + defer C.free(unsafe.Pointer(cAcceptor)) + rc = C.rdp_bridge_start_rdcleanpath_unix_fd( + C.int(dupFd), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + cDomain, + cAcceptor, + &handle, + ) + } if rc != C.RDP_BRIDGE_OK { return nil, fmt.Errorf("rdp bridge: start failed (status %d)", int32(rc)) } @@ -72,9 +93,17 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, return &Bridge{handle: uint64(handle)}, nil } -// Adapts an fd-less Go byte stream to the Rust bridge (which needs a real fd -// for tokio's TcpStream::from_raw_fd) by routing through a loopback TCP pair. +// Routes fd-less Go streams through a loopback TCP pair for tokio. func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) { + return startWithReadWriterCommon(rw, targetHost, targetPort, username, password, domain, "") +} + +// Browser-flow analog of StartWithReadWriter. +func StartRDCleanPathWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { + return startWithReadWriterCommon(rw, targetHost, targetPort, username, password, domain, acceptorUsername) +} + +func startWithReadWriterCommon(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) @@ -109,7 +138,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, fmt.Errorf("rdp bridge: dup accepted fd: %w", err) } - bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain) + bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain, acceptorUsername) if err != nil { _ = peer.Close() return nil, err diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index d706b8ee..6608d430 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -26,10 +26,20 @@ func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username if err != nil { return nil, fmt.Errorf("rdp bridge: dup client socket: %w", err) } - return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain) + return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain, "") } -func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) { +// Browser-flow analog of StartWithConn. +func StartRDCleanPathWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { + dupSocket, err := dupConnSocket(conn) + if err != nil { + return nil, fmt.Errorf("rdp bridge: dup client socket: %w", err) + } + return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain, acceptorUsername) +} + +// Empty acceptorUsername selects native flow; non-empty selects RDCleanPath. +func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { success := false defer func() { if !success { @@ -51,15 +61,31 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor } var handle C.uint64_t - rc := C.rdp_bridge_start_windows_socket( - C.uintptr_t(dupSocket), - cHost, - C.uint16_t(targetPort), - cUser, - cPass, - cDomain, - &handle, - ) + var rc C.int32_t + if acceptorUsername == "" { + rc = C.rdp_bridge_start_windows_socket( + C.uintptr_t(dupSocket), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + cDomain, + &handle, + ) + } else { + cAcceptor := C.CString(acceptorUsername) + defer C.free(unsafe.Pointer(cAcceptor)) + rc = C.rdp_bridge_start_rdcleanpath_windows_socket( + C.uintptr_t(dupSocket), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + cDomain, + cAcceptor, + &handle, + ) + } if rc != C.RDP_BRIDGE_OK { return nil, fmt.Errorf("rdp bridge: start failed (status %d)", int32(rc)) } @@ -68,6 +94,15 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor } func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) { + return startWithReadWriterCommon(rw, targetHost, targetPort, username, password, domain, "") +} + +// Browser-flow analog of StartWithReadWriter. +func StartRDCleanPathWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { + return startWithReadWriterCommon(rw, targetHost, targetPort, username, password, domain, acceptorUsername) +} + +func startWithReadWriterCommon(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain, acceptorUsername string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) @@ -102,7 +137,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, fmt.Errorf("rdp bridge: dup accepted socket: %w", err) } - bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain) + bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain, acceptorUsername) if err != nil { _ = peer.Close() return nil, err diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index 28da7815..aa7f9b13 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -17,15 +17,28 @@ func StartWithConn(_ net.Conn, _ string, _ uint16, _, _, _ string) (*Bridge, err return nil, ErrRdpUnavailable } +func StartRDCleanPathWithConn(_ net.Conn, _ string, _ uint16, _, _, _, _ string) (*Bridge, error) { + return nil, ErrRdpUnavailable +} + func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } +func StartRDCleanPathWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _, _, _ string) (*Bridge, error) { + return nil, ErrRdpUnavailable +} + func (p *RDPProxy) HandleConnection(_ context.Context, clientConn net.Conn) error { _ = clientConn.Close() return ErrRdpUnavailable } +func (p *RDPProxy) HandleConnectionRDCleanPath(_ context.Context, clientConn net.Conn) error { + _ = clientConn.Close() + return ErrRdpUnavailable +} + func (b *Bridge) Wait() error { return ErrRdpUnavailable } func (b *Bridge) Cancel() error { return ErrRdpUnavailable } func (b *Bridge) Close() error { return ErrRdpUnavailable } diff --git a/packages/pam/handlers/rdp/native/Cargo.lock b/packages/pam/handlers/rdp/native/Cargo.lock index c4652505..edfc4a04 100644 --- a/packages/pam/handlers/rdp/native/Cargo.lock +++ b/packages/pam/handlers/rdp/native/Cargo.lock @@ -1311,6 +1311,7 @@ dependencies = [ "ironrdp-connector", "ironrdp-core", "ironrdp-pdu", + "ironrdp-rdcleanpath", "ironrdp-tls", "ironrdp-tokio", "libc", @@ -1448,6 +1449,15 @@ dependencies = [ "x509-cert", ] +[[package]] +name = "ironrdp-rdcleanpath" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e9011b8f48be356a51c75757a09075af787601a03542815424da493d2d2757" +dependencies = [ + "der 0.7.10", +] + [[package]] name = "ironrdp-svc" version = "0.6.0" diff --git a/packages/pam/handlers/rdp/native/Cargo.toml b/packages/pam/handlers/rdp/native/Cargo.toml index cb53a5d2..52ef126c 100644 --- a/packages/pam/handlers/rdp/native/Cargo.toml +++ b/packages/pam/handlers/rdp/native/Cargo.toml @@ -17,6 +17,7 @@ ironrdp-core = "0.1" ironrdp-tokio = { version = "0.8", features = ["reqwest"] } ironrdp-pdu = "0.7" ironrdp-tls = { version = "0.2", features = ["rustls"] } +ironrdp-rdcleanpath = "0.2" x509-cert = { version = "0.2", features = ["std"] } libc = "0.2" diff --git a/packages/pam/handlers/rdp/native/include/rdp_bridge.h b/packages/pam/handlers/rdp/native/include/rdp_bridge.h index 150c6b6f..9d9f39cc 100644 --- a/packages/pam/handlers/rdp/native/include/rdp_bridge.h +++ b/packages/pam/handlers/rdp/native/include/rdp_bridge.h @@ -44,6 +44,37 @@ int32_t rdp_bridge_start_windows_socket( ); #endif +/* Browser-flow start. Same as the native start, plus `acceptor_username`: + * the username the browser is configured to present during the acceptor's + * CredSSP exchange (decoupled from the real target username injected into + * the connector). The browser speaks RDCleanPath over the inbound stream; + * the gateway's WS upstream has already stripped framing. */ +#if defined(__unix__) || defined(__APPLE__) +int32_t rdp_bridge_start_rdcleanpath_unix_fd( + int client_fd, + const char *target_host, + uint16_t target_port, + const char *username, + const char *password, + const char *domain, + const char *acceptor_username, + uint64_t *out_handle +); +#endif + +#if defined(_WIN32) || defined(_WIN64) +int32_t rdp_bridge_start_rdcleanpath_windows_socket( + uintptr_t client_socket, + const char *target_host, + uint16_t target_port, + const char *username, + const char *password, + const char *domain, + const char *acceptor_username, + uint64_t *out_handle +); +#endif + int32_t rdp_bridge_wait(uint64_t handle); int32_t rdp_bridge_cancel(uint64_t handle); int32_t rdp_bridge_free(uint64_t handle); diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 98503597..434bd5f5 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -118,7 +118,7 @@ async fn run_mitm_inner( bridge_pdus(client_framed, target_framed, tx).await } -async fn bridge_pdus( +pub async fn bridge_pdus( client_framed: ironrdp_tokio::TokioFramed, target_framed: ironrdp_tokio::TokioFramed, tx: EventSender, @@ -465,7 +465,7 @@ fn decode_fast_path_input(frame: &[u8]) -> anyhow::Result { // wrong value, and target servers reject mismatched echoes) // - clear CS_NET.channels so the target doesn't try to open virtual // channels (clipboard, drives, audio, USB) the bridge can't service -async fn filter_client_mcs_connect_initial( +pub(crate) async fn filter_client_mcs_connect_initial( client_stream: &mut ErasedStream, target_stream: &mut ErasedStream, leftover: bytes::BytesMut, @@ -538,8 +538,8 @@ async fn run_acceptor_half( client_tcp: TcpStream, username: String, ) -> Result<(ErasedStream, bytes::BytesMut)> { - let (server_tls, acceptor_public_key) = - build_acceptor_tls().context("build acceptor TLS config")?; + let (server_tls, acceptor_public_key, _cert_der) = + build_acceptor_tls_with_cert().context("build acceptor TLS config")?; let server_tls = Arc::new(server_tls); let acceptor_framed = ironrdp_tokio::TokioFramed::new(client_tcp); @@ -716,7 +716,7 @@ where // Replicated from ironrdp-async's private perform_credssp_step so we can // stop before connect_finalize (which would start MCS/capability exchange). -async fn perform_connector_credssp( +pub(crate) async fn perform_connector_credssp( connector: &mut ClientConnector, framed: &mut ironrdp_tokio::TokioFramed, network_client: &mut ReqwestNetworkClient, @@ -799,7 +799,8 @@ where Ok(()) } -fn build_acceptor_tls() -> Result<(tokio_rustls::rustls::ServerConfig, Vec)> { +pub(crate) fn build_acceptor_tls_with_cert( +) -> Result<(tokio_rustls::rustls::ServerConfig, Vec, Vec)> { use x509_cert::der::Decode; let subject_alt_names = vec!["localhost".to_string(), "infisical-rdp-bridge".to_string()]; @@ -813,13 +814,14 @@ fn build_acceptor_tls() -> Result<(tokio_rustls::rustls::ServerConfig, Vec)> .ok_or_else(|| anyhow::anyhow!("extract public key from self-signed cert"))? .to_vec(); + let cert_der_bytes = cert_der.as_ref().to_vec(); let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into()); let config = tokio_rustls::rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert_der], key_der) .context("rustls ServerConfig")?; - Ok((config, public_key)) + Ok((config, public_key, cert_der_bytes)) } pub trait AsyncReadWrite: AsyncRead + AsyncWrite {} diff --git a/packages/pam/handlers/rdp/native/src/caps.rs b/packages/pam/handlers/rdp/native/src/caps.rs new file mode 100644 index 00000000..44cd00b7 --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/caps.rs @@ -0,0 +1,98 @@ +//! Capability sets advertised by the acceptor (browser-facing half). +//! +//! Copied almost verbatim from `ironrdp-server::capabilities`. The goal is a +//! conservative, widely-supported set so both the inbound client and the +//! outbound target agree on formats without transcoding. Advanced codecs +//! (RemoteFX, EGFX) are left out deliberately: we want both handshakes to +//! land on basic bitmap. + +use ironrdp_pdu::rdp::capability_sets::{ + self, BitmapCodecs, BitmapDrawingFlags, CapabilitySet, CmdFlags, GeneralExtraFlags, InputFlags, + OrderFlags, OrderSupportExFlags, VirtualChannelFlags, server_codecs_capabilities, +}; + +const DEFAULT_WIDTH: u16 = 1920; +const DEFAULT_HEIGHT: u16 = 1080; +const MULTIFRAGMENT_MAX_REQUEST_SIZE: u32 = 8 * 1024 * 1024; + +pub fn acceptor_capabilities(width: u16, height: u16) -> Vec { + vec![ + CapabilitySet::General(general()), + CapabilitySet::Bitmap(bitmap(width, height)), + CapabilitySet::Order(order()), + CapabilitySet::SurfaceCommands(surface()), + CapabilitySet::Pointer(pointer()), + CapabilitySet::Input(input()), + CapabilitySet::VirtualChannel(virtual_channel()), + CapabilitySet::MultiFragmentUpdate(capability_sets::MultifragmentUpdate { + max_request_size: MULTIFRAGMENT_MAX_REQUEST_SIZE, + }), + // Advertise RemoteFX to the browser-side client. Matched by + // the connector config so both handshakes agree and the bridge + // forwards RFX-encoded bitmap updates byte-for-byte. + CapabilitySet::BitmapCodecs( + server_codecs_capabilities(&[]).unwrap_or_else(|_| BitmapCodecs(Vec::new())), + ), + ] +} + +pub fn default_desktop_size() -> (u16, u16) { + (DEFAULT_WIDTH, DEFAULT_HEIGHT) +} + +fn general() -> capability_sets::General { + capability_sets::General { + extra_flags: GeneralExtraFlags::FASTPATH_OUTPUT_SUPPORTED, + ..Default::default() + } +} + +fn bitmap(width: u16, height: u16) -> capability_sets::Bitmap { + capability_sets::Bitmap { + pref_bits_per_pix: 32, + desktop_width: width, + desktop_height: height, + desktop_resize_flag: true, + drawing_flags: BitmapDrawingFlags::empty(), + } +} + +fn order() -> capability_sets::Order { + capability_sets::Order::new(OrderFlags::empty(), OrderSupportExFlags::empty(), 2048, 224) +} + +fn surface() -> capability_sets::SurfaceCommands { + capability_sets::SurfaceCommands { + flags: CmdFlags::all(), + } +} + +fn pointer() -> capability_sets::Pointer { + capability_sets::Pointer { + color_pointer_cache_size: 2048, + pointer_cache_size: 2048, + } +} + +fn input() -> capability_sets::Input { + capability_sets::Input { + input_flags: InputFlags::SCANCODES + | InputFlags::MOUSE_RELATIVE + | InputFlags::MOUSEX + | InputFlags::FASTPATH_INPUT + | InputFlags::UNICODE + | InputFlags::FASTPATH_INPUT_2, + keyboard_layout: 0, + keyboard_type: None, + keyboard_subtype: 0, + keyboard_function_key: 128, + keyboard_ime_filename: String::new(), + } +} + +fn virtual_channel() -> capability_sets::VirtualChannel { + capability_sets::VirtualChannel { + flags: VirtualChannelFlags::NO_COMPRESSION, + chunk_size: None, + } +} diff --git a/packages/pam/handlers/rdp/native/src/config.rs b/packages/pam/handlers/rdp/native/src/config.rs index d959fe18..d70a3d1f 100644 --- a/packages/pam/handlers/rdp/native/src/config.rs +++ b/packages/pam/handlers/rdp/native/src/config.rs @@ -3,7 +3,7 @@ use ironrdp_connector::{BitmapConfig, Config, Credentials, DesktopSize}; use ironrdp_pdu::gcc::KeyboardType; -use ironrdp_pdu::rdp::capability_sets::{BitmapCodecs, MajorPlatformType}; +use ironrdp_pdu::rdp::capability_sets::{client_codecs_capabilities, BitmapCodecs, MajorPlatformType}; use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; pub const DEFAULT_WIDTH: u16 = 1920; @@ -54,3 +54,42 @@ pub fn connector_config(username: String, password: String, domain: Option Config { + Config { + desktop_size: DesktopSize { width, height }, + desktop_scale_factor: 0, + + enable_tls: false, + enable_credssp: true, + + credentials: Credentials::UsernamePassword { username, password }, + domain: None, + + client_build: 0, + client_name: "infisical-pam".to_owned(), + keyboard_type: KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_functional_keys_count: 12, + keyboard_layout: 0, + ime_file_name: String::new(), + + bitmap: Some(BitmapConfig { + lossy_compression: false, + color_depth: 32, + codecs: client_codecs_capabilities(&[]).unwrap_or_else(|_| BitmapCodecs(Vec::new())), + }), + dig_product_id: String::new(), + client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), + platform: MajorPlatformType::UNSPECIFIED, + hardware_id: None, + request_data: None, + autologon: false, + enable_audio_playback: false, + performance_flags: PerformanceFlags::default(), + license_cache: None, + timezone_info: TimezoneInfo::default(), + enable_server_pointer: false, + pointer_software_rendering: false, + } +} diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index fb637e99..9a5f2d89 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -16,6 +16,7 @@ use tracing::{error, info}; use crate::bridge::{run_mitm, TargetEndpoint}; use crate::events::{self, SessionEvent}; +use crate::rdcleanpath::run_mitm_rdcleanpath; pub const RDP_BRIDGE_OK: i32 = 0; pub const RDP_BRIDGE_SESSION_ERROR: i32 = 1; @@ -189,6 +190,19 @@ unsafe fn c_str_to_owned(ptr: *const c_char) -> Option { .map(str::to_owned) } +/// Selects which MITM flow runs on the spawned session thread. Both flows +/// share the connector / event-tap halves and only differ in how the +/// acceptor is bootstrapped. +enum SessionFlow { + /// Native client → CLI loopback → gateway. Acceptor does X.224 + TLS + + /// CredSSP normally. + Native, + /// Browser → backend WS pump → gateway. Acceptor short-circuits X.224 + /// via RDCleanPath; `acceptor_username` is the browser-presented NLA + /// username (typically a fixed string like "infisical"). + Rdcleanpath { acceptor_username: String }, +} + fn spawn_session( client_tcp: StdTcpStream, host: String, @@ -196,6 +210,7 @@ fn spawn_session( username: String, password: String, domain: Option, + flow: SessionFlow, ) -> anyhow::Result { client_tcp.set_nonblocking(true)?; let cancel = CancellationToken::new(); @@ -218,7 +233,21 @@ fn spawn_session( password, domain, }; - run_mitm(client, endpoint, cancel_for_thread, events_tx).await + match flow { + SessionFlow::Native => { + run_mitm(client, endpoint, cancel_for_thread, events_tx).await + } + SessionFlow::Rdcleanpath { acceptor_username } => { + run_mitm_rdcleanpath( + client, + endpoint, + acceptor_username, + cancel_for_thread, + events_tx, + ) + .await + } + } }) })?; @@ -267,7 +296,15 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( use std::os::unix::io::FromRawFd; let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; - match spawn_session(client_tcp, host, target_port, username, password, domain) { + match spawn_session( + client_tcp, + host, + target_port, + username, + password, + domain, + SessionFlow::Native, + ) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK @@ -313,7 +350,15 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( use std::os::windows::io::{FromRawSocket, RawSocket}; let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; - match spawn_session(client_tcp, host, target_port, username, password, domain) { + match spawn_session( + client_tcp, + host, + target_port, + username, + password, + domain, + SessionFlow::Native, + ) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK @@ -325,6 +370,130 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( } } +/// Browser-flow start. Same shape as `rdp_bridge_start_unix_fd`, plus +/// `acceptor_username`: the username the browser is configured to present +/// during the acceptor-side CredSSP exchange (decoupled from the real +/// target username we inject into the connector). +/// +/// # Safety +/// +/// `client_fd` ownership transfers to the bridge on OK, stays with the +/// caller on error. Strings must be NUL-terminated valid UTF-8. +#[cfg(unix)] +#[no_mangle] +pub unsafe extern "C" fn rdp_bridge_start_rdcleanpath_unix_fd( + client_fd: std::ffi::c_int, + target_host: *const c_char, + target_port: u16, + username: *const c_char, + password: *const c_char, + domain: *const c_char, + acceptor_username: *const c_char, + out_handle: *mut u64, +) -> i32 { + if out_handle.is_null() { + return RDP_BRIDGE_BAD_ARG; + } + let host = match unsafe { c_str_to_owned(target_host) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let username = match unsafe { c_str_to_owned(username) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let password = match unsafe { c_str_to_owned(password) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty()); + let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { + Some(v) if !v.is_empty() => v, + _ => return RDP_BRIDGE_BAD_ARG, + }; + + use std::os::unix::io::FromRawFd; + let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; + + match spawn_session( + client_tcp, + host, + target_port, + username, + password, + domain, + SessionFlow::Rdcleanpath { acceptor_username }, + ) { + Ok(id) => { + unsafe { *out_handle = id }; + RDP_BRIDGE_OK + } + Err(e) => { + error!(error = ?e, "rdp_bridge_start_rdcleanpath_unix_fd: failed to spawn session"); + RDP_BRIDGE_RUNTIME_ERROR + } + } +} + +/// # Safety +/// +/// See `rdp_bridge_start_rdcleanpath_unix_fd`. +#[cfg(windows)] +#[no_mangle] +pub unsafe extern "C" fn rdp_bridge_start_rdcleanpath_windows_socket( + client_socket: usize, + target_host: *const c_char, + target_port: u16, + username: *const c_char, + password: *const c_char, + domain: *const c_char, + acceptor_username: *const c_char, + out_handle: *mut u64, +) -> i32 { + if out_handle.is_null() { + return RDP_BRIDGE_BAD_ARG; + } + let host = match unsafe { c_str_to_owned(target_host) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let username = match unsafe { c_str_to_owned(username) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let password = match unsafe { c_str_to_owned(password) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty()); + let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { + Some(v) if !v.is_empty() => v, + _ => return RDP_BRIDGE_BAD_ARG, + }; + + use std::os::windows::io::{FromRawSocket, RawSocket}; + let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; + + match spawn_session( + client_tcp, + host, + target_port, + username, + password, + domain, + SessionFlow::Rdcleanpath { acceptor_username }, + ) { + Ok(id) => { + unsafe { *out_handle = id }; + RDP_BRIDGE_OK + } + Err(e) => { + error!(error = ?e, "rdp_bridge_start_rdcleanpath_windows_socket: failed to spawn session"); + RDP_BRIDGE_RUNTIME_ERROR + } + } +} + #[no_mangle] pub extern "C" fn rdp_bridge_wait(handle: u64) -> i32 { let join = { @@ -343,12 +512,10 @@ pub extern "C" fn rdp_bridge_wait(handle: u64) -> i32 { } Ok(Err(e)) => { error!(handle, error = ?e, "rdp_bridge_wait: session failed"); - eprintln!("rdp bridge session failed (handle={handle}): {e:?}"); RDP_BRIDGE_SESSION_ERROR } Err(panic) => { error!(handle, "rdp_bridge_wait: session thread panicked"); - eprintln!("rdp bridge session thread panicked (handle={handle}): {panic:?}"); RDP_BRIDGE_THREAD_PANIC } }, diff --git a/packages/pam/handlers/rdp/native/src/lib.rs b/packages/pam/handlers/rdp/native/src/lib.rs index abb6f0bd..b92496dd 100644 --- a/packages/pam/handlers/rdp/native/src/lib.rs +++ b/packages/pam/handlers/rdp/native/src/lib.rs @@ -4,6 +4,8 @@ pub mod bridge; pub mod cap_filter; +pub mod caps; pub mod config; pub mod events; pub mod ffi; +pub mod rdcleanpath; diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs new file mode 100644 index 00000000..44dec494 --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -0,0 +1,309 @@ +use anyhow::{anyhow, Context, Result}; +use ironrdp_acceptor::{accept_finalize, Acceptor, AcceptorResult, DesktopSize as AcceptorDesktopSize}; +use ironrdp_connector::{ClientConnector, Sequence}; +use ironrdp_core::{encode_buf, WriteBuf}; +use ironrdp_pdu::nego::{ + ConnectionConfirm, ConnectionRequest, RequestFlags, ResponseFlags, SecurityProtocol, +}; +use ironrdp_pdu::rdp::client_info::Credentials as AcceptorCredentials; +use ironrdp_pdu::x224::X224; +use ironrdp_rdcleanpath::{DetectionResult, RDCleanPath, RDCleanPathPdu}; +use ironrdp_tokio::reqwest::ReqwestNetworkClient; +use ironrdp_tokio::{ + connect_finalize, mark_as_upgraded, skip_connect_begin, FramedWrite, TokioFramed, +}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::time::{timeout, Duration}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info, warn}; + +use crate::bridge::{bridge_pdus, ErasedStream, TargetEndpoint}; +use crate::caps::{acceptor_capabilities, default_desktop_size}; +use crate::config::connector_config_browser; +use crate::events::EventSender; + +pub async fn run_mitm_rdcleanpath( + client_tcp: TcpStream, + target: TargetEndpoint, + acceptor_username: String, + cancel: CancellationToken, + tx: EventSender, +) -> Result<()> { + tokio::select! { + result = handle_browser_session(client_tcp, target, acceptor_username, tx) => result, + _ = cancel.cancelled() => { + info!("rdcleanpath session canceled by caller"); + Ok(()) + } + } +} + +async fn handle_browser_session( + mut client_tcp: TcpStream, + target: TargetEndpoint, + acceptor_username: String, + tx: EventSender, +) -> Result<()> { + info!(host = %target.host, port = target.port, "rdcleanpath: starting browser session"); + let _ = rustls::crypto::ring::default_provider().install_default(); + + debug!("rdcleanpath: reading RDCleanPath request from client"); + let request_pdu = read_rdcleanpath_pdu(&mut client_tcp) + .await + .context("read RDCleanPath Request")?; + debug!("rdcleanpath: received RDCleanPath request"); + let request = request_pdu + .into_enum() + .map_err(|e| anyhow!("RDCleanPath enum: {e}"))?; + let (x224_cr, destination) = match request { + RDCleanPath::Request { + destination, + x224_connection_request, + .. + } => (x224_connection_request.as_bytes().to_vec(), destination), + other => anyhow::bail!("expected RDCleanPath::Request, got {other:?}"), + }; + info!(destination, "RDCleanPath: received Request"); + + debug!("rdcleanpath: connecting to target"); + let target_tcp = TcpStream::connect((target.host.as_str(), target.port)) + .await + .with_context(|| format!("connect target {}:{}", target.host, target.port))?; + debug!("rdcleanpath: target TCP connected"); + let target_addr = target_tcp.local_addr().context("local_addr")?; + let mut target_framed = TokioFramed::new(target_tcp); + + target_framed + .write_all(&x224_cr) + .await + .context("write X.224 CR to target")?; + let x224_cc_target = read_tpkt_pdu(&mut target_framed) + .await + .context("read X.224 CC")?; + debug!(len = x224_cc_target.len(), "received X.224 CC from target"); + + let (initial_stream, leftover) = target_framed.into_inner(); + debug!("rdcleanpath: TLS upgrading target"); + let (upgraded_stream, target_cert) = ironrdp_tls::upgrade(initial_stream, &target.host) + .await + .context("TLS upgrade to target")?; + debug!("rdcleanpath: target TLS upgraded"); + + let throwaway_cert_der = generate_throwaway_cert().context("throwaway cert")?; + + let fake_cc_bytes = encode_x224(X224(ConnectionConfirm::Response { + flags: ResponseFlags::empty(), + protocol: SecurityProtocol::SSL, + })) + .context("encode SSL-only X.224 CC")?; + + let response = RDCleanPathPdu::new_response( + format!("{}:{}", target.host, target.port), + fake_cc_bytes, + std::iter::once(throwaway_cert_der), + ) + .map_err(|e| anyhow!("build RDCleanPath Response: {e:?}"))?; + let response_der = response + .to_der() + .map_err(|e| anyhow!("encode RDCleanPath Response: {e:?}"))?; + client_tcp + .write_all(&response_der) + .await + .context("write RDCleanPath Response to client")?; + debug!(len = response_der.len(), "rdcleanpath: sent RDCleanPath response"); + + let (width, height) = default_desktop_size(); + + let config = connector_config_browser( + target.username.clone(), + target.password.clone(), + width, + height, + ); + let mut connector = ClientConnector::new(config, target_addr); + + let mut scratch = WriteBuf::new(); + connector + .step_no_input(&mut scratch) + .map_err(|e| anyhow!("connector step_no_input: {e:?}"))?; + scratch.clear(); + connector + .step(&x224_cc_target, &mut scratch) + .map_err(|e| anyhow!("connector step CC: {e:?}"))?; + + let should_upgrade = skip_connect_begin(&mut connector); + let upgraded = mark_as_upgraded(should_upgrade, &mut connector); + + let target_erased: ErasedStream = Box::new(upgraded_stream); + let mut target_upgraded_framed = TokioFramed::new_with_leftover(target_erased, leftover); + + let server_public_key = ironrdp_tls::extract_tls_server_public_key(&target_cert) + .ok_or_else(|| anyhow!("extract target public key"))?; + + debug!("rdcleanpath: running connect_finalize on target"); + let connection_result = connect_finalize( + upgraded, + connector, + &mut target_upgraded_framed, + &mut ReqwestNetworkClient::new(), + ironrdp_connector::ServerName::new(&target.host), + server_public_key.to_owned(), + None, + ) + .await + .map_err(|e| anyhow!("target connect_finalize: {e:?}"))?; + info!( + width = connection_result.desktop_size.width, + height = connection_result.desktop_size.height, + "rdcleanpath: target reached active stage" + ); + + let placeholder_creds = AcceptorCredentials { + username: if acceptor_username.is_empty() { + "infisical".to_owned() + } else { + acceptor_username + }, + password: "infisical".to_owned(), + domain: None, + }; + let mut acceptor = Acceptor::new( + SecurityProtocol::SSL, + AcceptorDesktopSize { width, height }, + acceptor_capabilities(width, height), + Some(placeholder_creds), + ); + + let fake_cr_bytes = encode_x224(X224(ConnectionRequest { + nego_data: None, + flags: RequestFlags::empty(), + protocol: SecurityProtocol::SSL, + })) + .context("encode SSL-only X.224 CR")?; + + let mut acc_scratch = WriteBuf::new(); + acceptor + .step(&fake_cr_bytes, &mut acc_scratch) + .map_err(|e| anyhow!("acceptor step CR: {e:?}"))?; + acc_scratch.clear(); + acceptor + .step(&[], &mut acc_scratch) + .map_err(|e| anyhow!("acceptor step empty: {e:?}"))?; + acc_scratch.clear(); + + if acceptor.reached_security_upgrade().is_none() { + anyhow::bail!("acceptor did not reach SecurityUpgrade after synthetic CR/CC"); + } + acceptor.mark_security_upgrade_as_done(); + + let client_erased: ErasedStream = Box::new(client_tcp); + let client_framed: TokioFramed = TokioFramed::new(client_erased); + + debug!("rdcleanpath: running accept_finalize on client"); + let (client_final_framed, acceptor_result): (TokioFramed, AcceptorResult) = + accept_finalize(client_framed, &mut acceptor) + .await + .map_err(|e| anyhow!("accept_finalize: {e:?}"))?; + info!( + user_ch = acceptor_result.user_channel_id, + io_ch = acceptor_result.io_channel_id, + "rdcleanpath: client reached active stage" + ); + + debug!("rdcleanpath: bridging PDUs"); + bridge_pdus(client_final_framed, target_upgraded_framed, tx).await +} + +const RDCLEANPATH_READ_TIMEOUT: Duration = Duration::from_secs(30); + +async fn read_rdcleanpath_pdu(tcp: &mut TcpStream) -> Result { + timeout(RDCLEANPATH_READ_TIMEOUT, read_rdcleanpath_pdu_inner(tcp)) + .await + .map_err(|_| anyhow!("timed out waiting for RDCleanPath PDU ({}s)", RDCLEANPATH_READ_TIMEOUT.as_secs()))? +} + +async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result { + let mut buf = Vec::with_capacity(512); + loop { + let mut chunk = [0u8; 512]; + let n = tcp + .read(&mut chunk) + .await + .context("read RDCleanPath bytes")?; + if n == 0 { + anyhow::bail!("peer closed during RDCleanPath PDU read"); + } + buf.extend_from_slice(&chunk[..n]); + match RDCleanPathPdu::detect(&buf) { + DetectionResult::Detected { total_length, .. } => { + if buf.len() >= total_length { + if buf.len() > total_length { + warn!( + extra = buf.len() - total_length, + "extra bytes after RDCleanPath PDU; ignoring" + ); + buf.truncate(total_length); + } + return RDCleanPathPdu::from_der(&buf) + .map_err(|e| anyhow!("decode RDCleanPath PDU: {e:?}")); + } + } + DetectionResult::NotEnoughBytes => {} + DetectionResult::Failed => { + anyhow::bail!("not a valid RDCleanPath PDU"); + } + } + if buf.len() > 64 * 1024 { + anyhow::bail!("RDCleanPath PDU exceeded 64KB while incomplete"); + } + } +} + +async fn read_tpkt_pdu(framed: &mut TokioFramed) -> Result> +where + S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Sync + Unpin + 'static, +{ + let (stream, _leftover) = framed.get_inner_mut(); + let mut header = [0u8; 4]; + stream + .read_exact(&mut header) + .await + .context("read TPKT header")?; + if header[0] != 0x03 { + anyhow::bail!("not a TPKT frame: {:02x}", header[0]); + } + let total_len = u16::from_be_bytes([header[2], header[3]]) as usize; + if total_len < 4 { + anyhow::bail!("TPKT length too small"); + } + let mut body = vec![0u8; total_len - 4]; + stream + .read_exact(&mut body) + .await + .context("read TPKT body")?; + let mut full = Vec::with_capacity(total_len); + full.extend_from_slice(&header); + full.extend_from_slice(&body); + Ok(full) +} + +fn encode_x224(pdu: T) -> Result> +where + T: ironrdp_core::Encode, +{ + let mut buf = WriteBuf::new(); + encode_buf(&pdu, &mut buf).map_err(|e| anyhow!("encode X.224 PDU: {e:?}"))?; + Ok(buf.filled().to_vec()) +} + +fn generate_throwaway_cert() -> Result> { + let subject_alt_names = vec![ + "localhost".to_string(), + "infisical-rdp-gateway".to_string(), + ]; + let cert = + rcgen::generate_simple_self_signed(subject_alt_names).context("generate self-signed cert")?; + Ok(cert.cert.der().to_vec()) +} + diff --git a/packages/pam/handlers/rdp/proxy.go b/packages/pam/handlers/rdp/proxy.go index 1c3f38cb..658f5c13 100644 --- a/packages/pam/handlers/rdp/proxy.go +++ b/packages/pam/handlers/rdp/proxy.go @@ -16,14 +16,10 @@ type RDPProxyConfig struct { TargetPort uint16 InjectUsername string InjectPassword string - // Empty for local accounts; AD domain name (e.g. "CORP.EXAMPLE.COM") for - // domain-joined NTLM CredSSP. Backend session credentials populate this. InjectDomain string SessionID string SessionLogger session.SessionLogger - // Session-anchored origin for elapsedNs. The Rust bridge restarts its - // own clock per RDP client connection; we rewrite each event's elapsedNs - // against this anchor so timestamps stay monotonic across reconnects. + // Rewrite bridge elapsedNs against this anchor so timestamps stay monotonic across reconnects SessionStartedAt time.Time } @@ -35,6 +31,9 @@ func NewRDPProxy(config RDPProxyConfig) *RDPProxy { return &RDPProxy{config: config} } +// Fixed NLA username the browser presents; carries no security weight +const BrowserAcceptorUsername = "infisical" + // Wire envelopes carried inside TerminalEvent.Data for ChannelType=RDP. type rdpTargetFrameEnvelope struct { Type string `json:"type"` // "target_frame" @@ -71,8 +70,6 @@ const pollTimeout = 250 * time.Millisecond var errUnknownRdpEventType = errors.New("rdp: unknown event type") -// Logger errors are warned but don't stop the drain; dropping one event is -// better than back-pressuring the bridge byte stream. func drainBridgeEvents(ctx context.Context, b *Bridge, logger session.SessionLogger, sessionID string, sessionStartedAt time.Time) { if logger == nil { return diff --git a/packages/pam/local/database-proxy.go b/packages/pam/local/database-proxy.go index 9f51f4e3..ef1cf6d5 100644 --- a/packages/pam/local/database-proxy.go +++ b/packages/pam/local/database-proxy.go @@ -26,6 +26,7 @@ type ALPN string const ( ALPNInfisicalPAMProxy ALPN = "infisical-pam-proxy" + ALPNInfisicalPAMRDPBrowser ALPN = "infisical-pam-rdp-browser" ALPNInfisicalPAMCancellation ALPN = "infisical-pam-session-cancellation" ALPNInfisicalPAMCapabilities ALPN = "infisical-pam-capabilities" ) diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index a128e763..de021915 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -164,8 +164,7 @@ func (p *RDPProxyServer) gracefulShutdown() { p.shutdownOnce.Do(func() { log.Info().Msg("Starting graceful shutdown of RDP proxy...") - // p.cancel() below can return main before this goroutine finishes; - // remove the .rdp file before risking that race. + // Remove before cancel() can return main if p.rdpFilePath != "" { if err := os.Remove(p.rdpFilePath); err != nil && !os.IsNotExist(err) { log.Debug().Err(err).Str("path", p.rdpFilePath).Msg("Failed to remove .rdp file on exit") @@ -233,9 +232,7 @@ func (p *RDPProxyServer) Run() { } } -// handleConnection forwards bytes between the RDP client and the gateway -// tunnel. Identical shape to the database proxy; the gateway's RDP -// handler takes over on the other side. +// handleConnection forwards bytes between the RDP client and the gateway tunnel. func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { defer func() { clientConn.Close() @@ -306,8 +303,7 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { log.Info().Msgf("RDP connection closed for client: %s", clientConn.RemoteAddr().String()) } -// Generates a per-session .rdp file under ~/.infisical/rdp/ pointing at -// the loopback listener. Removed on graceful shutdown. +// Generates a per-session .rdp file; removed on graceful shutdown. func writeRDPFile(listenPort int, sessionID, username string) (string, error) { filename := fmt.Sprintf("infisical-rdp-%s.rdp", sessionID) @@ -320,10 +316,7 @@ func writeRDPFile(listenPort int, sessionID, username string) (string, error) { } path := filepath.Join(dir, filename) - // authentication level:i:0 -> mstsc connects even if it can't verify the - // server's TLS cert. The bridge presents a self-signed cert, so without - // this mstsc terminates with "unexpected server authentication certificate". - // FreeRDP/Windows App ignore the cert by default; mstsc is the strict one. + // auth level 0: bridge presents self-signed cert, mstsc rejects without this content := fmt.Sprintf( "full address:s:127.0.0.1:%d\r\n"+ "username:s:%s\r\n"+ @@ -338,8 +331,7 @@ func writeRDPFile(listenPort int, sessionID, username string) (string, error) { return path, nil } -// rdpFileDir returns ~/.infisical/rdp (the conventional per-user state -// location for CLI data; see util.CONFIG_FOLDER_NAME). +// rdpFileDir returns ~/.infisical/rdp. func rdpFileDir() (string, error) { home, err := util.GetHomeDir() if err != nil { @@ -348,9 +340,7 @@ func rdpFileDir() (string, error) { return filepath.Join(home, util.CONFIG_FOLDER_NAME, "rdp"), nil } -// launchRDPClient opens the given .rdp file with the user's default RDP -// client. Failure is non-fatal; the caller can still manually connect -// using the printed connection details. +// launchRDPClient opens the .rdp file with the default client. Non-fatal on failure. func launchRDPClient(rdpFilePath string) error { var cmd *exec.Cmd switch runtime.GOOS { diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index bdfa6a86..c7b15164 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -139,7 +139,10 @@ func compilePolicyPatterns(config *api.PAMPolicyRuleConfig, sessionID string, ru return compiled } -func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMConfig, httpClient *resty.Client) error { +// HandlePAMProxy handles a PAM session connection. `browserRDP` selects +// the browser RDP flow (RDCleanPath over the inbound stream) when the +// resource is Windows; ignored for other resource types. +func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMConfig, httpClient *resty.Client, browserRDP bool) error { credentials, err := pamConfig.CredentialsManager.GetPAMSessionCredentials(pamConfig.SessionId, pamConfig.ExpiryTime) if err != nil { log.Error().Err(err).Str("sessionId", pamConfig.SessionId).Msg("Failed to retrieve PAM session credentials") @@ -434,7 +437,11 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo log.Info(). Str("sessionId", pamConfig.SessionId). Str("target", fmt.Sprintf("%s:%d", credentials.Host, credentials.Port)). + Bool("browser", browserRDP). Msg("Starting RDP PAM proxy") + if browserRDP { + return proxy.HandleConnectionRDCleanPath(ctx, conn) + } return proxy.HandleConnection(ctx, conn) default: return fmt.Errorf("unsupported resource type: %s", pamConfig.ResourceType) From 7aeed67d86a2f5c5992dd6d73e093e78f698b7d7 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 11 May 2026 00:29:55 -0400 Subject: [PATCH 02/16] fix(pam): address bot review issues in RDP browser session - Pass domain to connector_config_browser for AD credential injection - Preserve leftover bytes after RDCleanPath PDU parsing - Update caps.rs module doc to reflect actual codec advertisement --- packages/pam/handlers/rdp/native/src/caps.rs | 8 +++---- .../pam/handlers/rdp/native/src/config.rs | 4 ++-- .../handlers/rdp/native/src/rdcleanpath.rs | 24 +++++++++---------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/caps.rs b/packages/pam/handlers/rdp/native/src/caps.rs index 44cd00b7..838eb9d7 100644 --- a/packages/pam/handlers/rdp/native/src/caps.rs +++ b/packages/pam/handlers/rdp/native/src/caps.rs @@ -1,10 +1,8 @@ //! Capability sets advertised by the acceptor (browser-facing half). //! -//! Copied almost verbatim from `ironrdp-server::capabilities`. The goal is a -//! conservative, widely-supported set so both the inbound client and the -//! outbound target agree on formats without transcoding. Advanced codecs -//! (RemoteFX, EGFX) are left out deliberately: we want both handshakes to -//! land on basic bitmap. +//! Both the acceptor and connector advertise codec support (including RemoteFX) +//! so the browser client and target server agree on formats and the bridge can +//! forward encoded bitmap updates byte-for-byte without transcoding. use ironrdp_pdu::rdp::capability_sets::{ self, BitmapCodecs, BitmapDrawingFlags, CapabilitySet, CmdFlags, GeneralExtraFlags, InputFlags, diff --git a/packages/pam/handlers/rdp/native/src/config.rs b/packages/pam/handlers/rdp/native/src/config.rs index d70a3d1f..ba2d42c6 100644 --- a/packages/pam/handlers/rdp/native/src/config.rs +++ b/packages/pam/handlers/rdp/native/src/config.rs @@ -55,7 +55,7 @@ pub fn connector_config(username: String, password: String, domain: Option Config { +pub fn connector_config_browser(username: String, password: String, domain: Option, width: u16, height: u16) -> Config { Config { desktop_size: DesktopSize { width, height }, desktop_scale_factor: 0, @@ -64,7 +64,7 @@ pub fn connector_config_browser(username: String, password: String, width: u16, enable_credssp: true, credentials: Credentials::UsernamePassword { username, password }, - domain: None, + domain, client_build: 0, client_name: "infisical-pam".to_owned(), diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 44dec494..61d4c645 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -12,6 +12,7 @@ use ironrdp_tokio::reqwest::ReqwestNetworkClient; use ironrdp_tokio::{ connect_finalize, mark_as_upgraded, skip_connect_begin, FramedWrite, TokioFramed, }; +use bytes::BytesMut; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::{timeout, Duration}; @@ -49,7 +50,7 @@ async fn handle_browser_session( let _ = rustls::crypto::ring::default_provider().install_default(); debug!("rdcleanpath: reading RDCleanPath request from client"); - let request_pdu = read_rdcleanpath_pdu(&mut client_tcp) + let (request_pdu, client_leftover) = read_rdcleanpath_pdu(&mut client_tcp) .await .context("read RDCleanPath Request")?; debug!("rdcleanpath: received RDCleanPath request"); @@ -118,6 +119,7 @@ async fn handle_browser_session( let config = connector_config_browser( target.username.clone(), target.password.clone(), + target.domain.clone(), width, height, ); @@ -198,7 +200,7 @@ async fn handle_browser_session( acceptor.mark_security_upgrade_as_done(); let client_erased: ErasedStream = Box::new(client_tcp); - let client_framed: TokioFramed = TokioFramed::new(client_erased); + let client_framed: TokioFramed = TokioFramed::new_with_leftover(client_erased, client_leftover); debug!("rdcleanpath: running accept_finalize on client"); let (client_final_framed, acceptor_result): (TokioFramed, AcceptorResult) = @@ -217,13 +219,13 @@ async fn handle_browser_session( const RDCLEANPATH_READ_TIMEOUT: Duration = Duration::from_secs(30); -async fn read_rdcleanpath_pdu(tcp: &mut TcpStream) -> Result { +async fn read_rdcleanpath_pdu(tcp: &mut TcpStream) -> Result<(RDCleanPathPdu, BytesMut)> { timeout(RDCLEANPATH_READ_TIMEOUT, read_rdcleanpath_pdu_inner(tcp)) .await .map_err(|_| anyhow!("timed out waiting for RDCleanPath PDU ({}s)", RDCLEANPATH_READ_TIMEOUT.as_secs()))? } -async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result { +async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result<(RDCleanPathPdu, BytesMut)> { let mut buf = Vec::with_capacity(512); loop { let mut chunk = [0u8; 512]; @@ -238,15 +240,11 @@ async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result { if buf.len() >= total_length { - if buf.len() > total_length { - warn!( - extra = buf.len() - total_length, - "extra bytes after RDCleanPath PDU; ignoring" - ); - buf.truncate(total_length); - } - return RDCleanPathPdu::from_der(&buf) - .map_err(|e| anyhow!("decode RDCleanPath PDU: {e:?}")); + let leftover = BytesMut::from(&buf[total_length..]); + buf.truncate(total_length); + let pdu = RDCleanPathPdu::from_der(&buf) + .map_err(|e| anyhow!("decode RDCleanPath PDU: {e:?}"))?; + return Ok((pdu, leftover)); } } DetectionResult::NotEnoughBytes => {} From d56cde6641fc89f3941c6cf53fd5d3d522567ccc Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 11 May 2026 00:44:21 -0400 Subject: [PATCH 03/16] style(pam): apply cargo fmt formatting --- packages/pam/handlers/rdp/native/src/caps.rs | 4 +-- .../pam/handlers/rdp/native/src/config.rs | 12 ++++++-- .../handlers/rdp/native/src/rdcleanpath.rs | 30 ++++++++++++------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/caps.rs b/packages/pam/handlers/rdp/native/src/caps.rs index 838eb9d7..0f036ad1 100644 --- a/packages/pam/handlers/rdp/native/src/caps.rs +++ b/packages/pam/handlers/rdp/native/src/caps.rs @@ -5,8 +5,8 @@ //! forward encoded bitmap updates byte-for-byte without transcoding. use ironrdp_pdu::rdp::capability_sets::{ - self, BitmapCodecs, BitmapDrawingFlags, CapabilitySet, CmdFlags, GeneralExtraFlags, InputFlags, - OrderFlags, OrderSupportExFlags, VirtualChannelFlags, server_codecs_capabilities, + self, server_codecs_capabilities, BitmapCodecs, BitmapDrawingFlags, CapabilitySet, CmdFlags, + GeneralExtraFlags, InputFlags, OrderFlags, OrderSupportExFlags, VirtualChannelFlags, }; const DEFAULT_WIDTH: u16 = 1920; diff --git a/packages/pam/handlers/rdp/native/src/config.rs b/packages/pam/handlers/rdp/native/src/config.rs index ba2d42c6..a879425a 100644 --- a/packages/pam/handlers/rdp/native/src/config.rs +++ b/packages/pam/handlers/rdp/native/src/config.rs @@ -3,7 +3,9 @@ use ironrdp_connector::{BitmapConfig, Config, Credentials, DesktopSize}; use ironrdp_pdu::gcc::KeyboardType; -use ironrdp_pdu::rdp::capability_sets::{client_codecs_capabilities, BitmapCodecs, MajorPlatformType}; +use ironrdp_pdu::rdp::capability_sets::{ + client_codecs_capabilities, BitmapCodecs, MajorPlatformType, +}; use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; pub const DEFAULT_WIDTH: u16 = 1920; @@ -55,7 +57,13 @@ pub fn connector_config(username: String, password: String, domain: Option, width: u16, height: u16) -> Config { +pub fn connector_config_browser( + username: String, + password: String, + domain: Option, + width: u16, + height: u16, +) -> Config { Config { desktop_size: DesktopSize { width, height }, desktop_scale_factor: 0, diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 61d4c645..89b81fef 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -1,5 +1,8 @@ use anyhow::{anyhow, Context, Result}; -use ironrdp_acceptor::{accept_finalize, Acceptor, AcceptorResult, DesktopSize as AcceptorDesktopSize}; +use bytes::BytesMut; +use ironrdp_acceptor::{ + accept_finalize, Acceptor, AcceptorResult, DesktopSize as AcceptorDesktopSize, +}; use ironrdp_connector::{ClientConnector, Sequence}; use ironrdp_core::{encode_buf, WriteBuf}; use ironrdp_pdu::nego::{ @@ -12,7 +15,6 @@ use ironrdp_tokio::reqwest::ReqwestNetworkClient; use ironrdp_tokio::{ connect_finalize, mark_as_upgraded, skip_connect_begin, FramedWrite, TokioFramed, }; -use bytes::BytesMut; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::{timeout, Duration}; @@ -112,7 +114,10 @@ async fn handle_browser_session( .write_all(&response_der) .await .context("write RDCleanPath Response to client")?; - debug!(len = response_der.len(), "rdcleanpath: sent RDCleanPath response"); + debug!( + len = response_der.len(), + "rdcleanpath: sent RDCleanPath response" + ); let (width, height) = default_desktop_size(); @@ -200,7 +205,8 @@ async fn handle_browser_session( acceptor.mark_security_upgrade_as_done(); let client_erased: ErasedStream = Box::new(client_tcp); - let client_framed: TokioFramed = TokioFramed::new_with_leftover(client_erased, client_leftover); + let client_framed: TokioFramed = + TokioFramed::new_with_leftover(client_erased, client_leftover); debug!("rdcleanpath: running accept_finalize on client"); let (client_final_framed, acceptor_result): (TokioFramed, AcceptorResult) = @@ -222,7 +228,12 @@ const RDCLEANPATH_READ_TIMEOUT: Duration = Duration::from_secs(30); async fn read_rdcleanpath_pdu(tcp: &mut TcpStream) -> Result<(RDCleanPathPdu, BytesMut)> { timeout(RDCLEANPATH_READ_TIMEOUT, read_rdcleanpath_pdu_inner(tcp)) .await - .map_err(|_| anyhow!("timed out waiting for RDCleanPath PDU ({}s)", RDCLEANPATH_READ_TIMEOUT.as_secs()))? + .map_err(|_| { + anyhow!( + "timed out waiting for RDCleanPath PDU ({}s)", + RDCLEANPATH_READ_TIMEOUT.as_secs() + ) + })? } async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result<(RDCleanPathPdu, BytesMut)> { @@ -296,12 +307,9 @@ where } fn generate_throwaway_cert() -> Result> { - let subject_alt_names = vec![ - "localhost".to_string(), - "infisical-rdp-gateway".to_string(), - ]; - let cert = - rcgen::generate_simple_self_signed(subject_alt_names).context("generate self-signed cert")?; + let subject_alt_names = vec!["localhost".to_string(), "infisical-rdp-gateway".to_string()]; + let cert = rcgen::generate_simple_self_signed(subject_alt_names) + .context("generate self-signed cert")?; Ok(cert.cert.der().to_vec()) } From f53a670b5274bb0d439332dbcaad319f67dae86e Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 17:13:55 -0400 Subject: [PATCH 04/16] style(pam-rdp): remove trailing blank line for cargo fmt --- packages/pam/handlers/rdp/native/src/rdcleanpath.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 89b81fef..6af50c07 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -312,4 +312,3 @@ fn generate_throwaway_cert() -> Result> { .context("generate self-signed cert")?; Ok(cert.cert.der().to_vec()) } - From c6dcfb171bd89b7e5336e6a94106bca346a871d6 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 17:51:43 -0400 Subject: [PATCH 05/16] refactor(pam-rdp): reuse native bridge path for browser RDP Stop after CredSSP on both sides and bridge MCS/capability PDUs through filter_client_mcs_connect_initial, matching the native client flow. Removes caps.rs, connector_config_browser, and the independent accept_finalize/connect_finalize calls. --- packages/pam/handlers/rdp/native/src/caps.rs | 96 ------------- .../pam/handlers/rdp/native/src/config.rs | 49 +------ packages/pam/handlers/rdp/native/src/lib.rs | 1 - .../handlers/rdp/native/src/rdcleanpath.rs | 134 ++++++++++-------- 4 files changed, 79 insertions(+), 201 deletions(-) delete mode 100644 packages/pam/handlers/rdp/native/src/caps.rs diff --git a/packages/pam/handlers/rdp/native/src/caps.rs b/packages/pam/handlers/rdp/native/src/caps.rs deleted file mode 100644 index 0f036ad1..00000000 --- a/packages/pam/handlers/rdp/native/src/caps.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Capability sets advertised by the acceptor (browser-facing half). -//! -//! Both the acceptor and connector advertise codec support (including RemoteFX) -//! so the browser client and target server agree on formats and the bridge can -//! forward encoded bitmap updates byte-for-byte without transcoding. - -use ironrdp_pdu::rdp::capability_sets::{ - self, server_codecs_capabilities, BitmapCodecs, BitmapDrawingFlags, CapabilitySet, CmdFlags, - GeneralExtraFlags, InputFlags, OrderFlags, OrderSupportExFlags, VirtualChannelFlags, -}; - -const DEFAULT_WIDTH: u16 = 1920; -const DEFAULT_HEIGHT: u16 = 1080; -const MULTIFRAGMENT_MAX_REQUEST_SIZE: u32 = 8 * 1024 * 1024; - -pub fn acceptor_capabilities(width: u16, height: u16) -> Vec { - vec![ - CapabilitySet::General(general()), - CapabilitySet::Bitmap(bitmap(width, height)), - CapabilitySet::Order(order()), - CapabilitySet::SurfaceCommands(surface()), - CapabilitySet::Pointer(pointer()), - CapabilitySet::Input(input()), - CapabilitySet::VirtualChannel(virtual_channel()), - CapabilitySet::MultiFragmentUpdate(capability_sets::MultifragmentUpdate { - max_request_size: MULTIFRAGMENT_MAX_REQUEST_SIZE, - }), - // Advertise RemoteFX to the browser-side client. Matched by - // the connector config so both handshakes agree and the bridge - // forwards RFX-encoded bitmap updates byte-for-byte. - CapabilitySet::BitmapCodecs( - server_codecs_capabilities(&[]).unwrap_or_else(|_| BitmapCodecs(Vec::new())), - ), - ] -} - -pub fn default_desktop_size() -> (u16, u16) { - (DEFAULT_WIDTH, DEFAULT_HEIGHT) -} - -fn general() -> capability_sets::General { - capability_sets::General { - extra_flags: GeneralExtraFlags::FASTPATH_OUTPUT_SUPPORTED, - ..Default::default() - } -} - -fn bitmap(width: u16, height: u16) -> capability_sets::Bitmap { - capability_sets::Bitmap { - pref_bits_per_pix: 32, - desktop_width: width, - desktop_height: height, - desktop_resize_flag: true, - drawing_flags: BitmapDrawingFlags::empty(), - } -} - -fn order() -> capability_sets::Order { - capability_sets::Order::new(OrderFlags::empty(), OrderSupportExFlags::empty(), 2048, 224) -} - -fn surface() -> capability_sets::SurfaceCommands { - capability_sets::SurfaceCommands { - flags: CmdFlags::all(), - } -} - -fn pointer() -> capability_sets::Pointer { - capability_sets::Pointer { - color_pointer_cache_size: 2048, - pointer_cache_size: 2048, - } -} - -fn input() -> capability_sets::Input { - capability_sets::Input { - input_flags: InputFlags::SCANCODES - | InputFlags::MOUSE_RELATIVE - | InputFlags::MOUSEX - | InputFlags::FASTPATH_INPUT - | InputFlags::UNICODE - | InputFlags::FASTPATH_INPUT_2, - keyboard_layout: 0, - keyboard_type: None, - keyboard_subtype: 0, - keyboard_function_key: 128, - keyboard_ime_filename: String::new(), - } -} - -fn virtual_channel() -> capability_sets::VirtualChannel { - capability_sets::VirtualChannel { - flags: VirtualChannelFlags::NO_COMPRESSION, - chunk_size: None, - } -} diff --git a/packages/pam/handlers/rdp/native/src/config.rs b/packages/pam/handlers/rdp/native/src/config.rs index a879425a..d959fe18 100644 --- a/packages/pam/handlers/rdp/native/src/config.rs +++ b/packages/pam/handlers/rdp/native/src/config.rs @@ -3,9 +3,7 @@ use ironrdp_connector::{BitmapConfig, Config, Credentials, DesktopSize}; use ironrdp_pdu::gcc::KeyboardType; -use ironrdp_pdu::rdp::capability_sets::{ - client_codecs_capabilities, BitmapCodecs, MajorPlatformType, -}; +use ironrdp_pdu::rdp::capability_sets::{BitmapCodecs, MajorPlatformType}; use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; pub const DEFAULT_WIDTH: u16 = 1920; @@ -56,48 +54,3 @@ pub fn connector_config(username: String, password: String, domain: Option, - width: u16, - height: u16, -) -> Config { - Config { - desktop_size: DesktopSize { width, height }, - desktop_scale_factor: 0, - - enable_tls: false, - enable_credssp: true, - - credentials: Credentials::UsernamePassword { username, password }, - domain, - - client_build: 0, - client_name: "infisical-pam".to_owned(), - keyboard_type: KeyboardType::IbmEnhanced, - keyboard_subtype: 0, - keyboard_functional_keys_count: 12, - keyboard_layout: 0, - ime_file_name: String::new(), - - bitmap: Some(BitmapConfig { - lossy_compression: false, - color_depth: 32, - codecs: client_codecs_capabilities(&[]).unwrap_or_else(|_| BitmapCodecs(Vec::new())), - }), - dig_product_id: String::new(), - client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), - platform: MajorPlatformType::UNSPECIFIED, - hardware_id: None, - request_data: None, - autologon: false, - enable_audio_playback: false, - performance_flags: PerformanceFlags::default(), - license_cache: None, - timezone_info: TimezoneInfo::default(), - enable_server_pointer: false, - pointer_software_rendering: false, - } -} diff --git a/packages/pam/handlers/rdp/native/src/lib.rs b/packages/pam/handlers/rdp/native/src/lib.rs index b92496dd..13bf0bfe 100644 --- a/packages/pam/handlers/rdp/native/src/lib.rs +++ b/packages/pam/handlers/rdp/native/src/lib.rs @@ -4,7 +4,6 @@ pub mod bridge; pub mod cap_filter; -pub mod caps; pub mod config; pub mod events; pub mod ffi; diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 6af50c07..9af047b1 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -1,8 +1,6 @@ use anyhow::{anyhow, Context, Result}; use bytes::BytesMut; -use ironrdp_acceptor::{ - accept_finalize, Acceptor, AcceptorResult, DesktopSize as AcceptorDesktopSize, -}; +use ironrdp_acceptor::{Acceptor, DesktopSize as AcceptorDesktopSize}; use ironrdp_connector::{ClientConnector, Sequence}; use ironrdp_core::{encode_buf, WriteBuf}; use ironrdp_pdu::nego::{ @@ -12,18 +10,18 @@ use ironrdp_pdu::rdp::client_info::Credentials as AcceptorCredentials; use ironrdp_pdu::x224::X224; use ironrdp_rdcleanpath::{DetectionResult, RDCleanPath, RDCleanPathPdu}; use ironrdp_tokio::reqwest::ReqwestNetworkClient; -use ironrdp_tokio::{ - connect_finalize, mark_as_upgraded, skip_connect_begin, FramedWrite, TokioFramed, -}; +use ironrdp_tokio::{mark_as_upgraded, skip_connect_begin, FramedWrite, TokioFramed}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::{timeout, Duration}; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; -use crate::bridge::{bridge_pdus, ErasedStream, TargetEndpoint}; -use crate::caps::{acceptor_capabilities, default_desktop_size}; -use crate::config::connector_config_browser; +use crate::bridge::{ + bridge_pdus, build_acceptor_tls_with_cert, filter_client_mcs_connect_initial, + perform_connector_credssp, ErasedStream, TargetEndpoint, +}; +use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; use crate::events::EventSender; pub async fn run_mitm_rdcleanpath( @@ -86,14 +84,15 @@ async fn handle_browser_session( .context("read X.224 CC")?; debug!(len = x224_cc_target.len(), "received X.224 CC from target"); - let (initial_stream, leftover) = target_framed.into_inner(); + let (initial_stream, target_leftover) = target_framed.into_inner(); debug!("rdcleanpath: TLS upgrading target"); let (upgraded_stream, target_cert) = ironrdp_tls::upgrade(initial_stream, &target.host) .await .context("TLS upgrade to target")?; debug!("rdcleanpath: target TLS upgraded"); - let throwaway_cert_der = generate_throwaway_cert().context("throwaway cert")?; + let (_tls_config, acceptor_public_key, throwaway_cert_der) = + build_acceptor_tls_with_cert().context("build throwaway cert")?; let fake_cc_bytes = encode_x224(X224(ConnectionConfirm::Response { flags: ResponseFlags::empty(), @@ -119,14 +118,12 @@ async fn handle_browser_session( "rdcleanpath: sent RDCleanPath response" ); - let (width, height) = default_desktop_size(); + // --- Connector: advance past X.224, then CredSSP only --- - let config = connector_config_browser( + let config = connector_config( target.username.clone(), target.password.clone(), target.domain.clone(), - width, - height, ); let mut connector = ClientConnector::new(config, target_addr); @@ -140,31 +137,30 @@ async fn handle_browser_session( .map_err(|e| anyhow!("connector step CC: {e:?}"))?; let should_upgrade = skip_connect_begin(&mut connector); - let upgraded = mark_as_upgraded(should_upgrade, &mut connector); + let _ = mark_as_upgraded(should_upgrade, &mut connector); let target_erased: ErasedStream = Box::new(upgraded_stream); - let mut target_upgraded_framed = TokioFramed::new_with_leftover(target_erased, leftover); + let mut target_framed = TokioFramed::new_with_leftover(target_erased, target_leftover); let server_public_key = ironrdp_tls::extract_tls_server_public_key(&target_cert) .ok_or_else(|| anyhow!("extract target public key"))?; - debug!("rdcleanpath: running connect_finalize on target"); - let connection_result = connect_finalize( - upgraded, - connector, - &mut target_upgraded_framed, - &mut ReqwestNetworkClient::new(), - ironrdp_connector::ServerName::new(&target.host), - server_public_key.to_owned(), - None, - ) - .await - .map_err(|e| anyhow!("target connect_finalize: {e:?}"))?; - info!( - width = connection_result.desktop_size.width, - height = connection_result.desktop_size.height, - "rdcleanpath: target reached active stage" - ); + debug!("rdcleanpath: connector CredSSP"); + if connector.should_perform_credssp() { + perform_connector_credssp( + &mut connector, + &mut target_framed, + &mut ReqwestNetworkClient::new(), + ironrdp_connector::ServerName::new(&target.host), + server_public_key.to_vec(), + None, + ) + .await + .context("connector: CredSSP")?; + } + info!("rdcleanpath: connector CredSSP complete"); + + // --- Acceptor: advance past X.224, then CredSSP only --- let placeholder_creds = AcceptorCredentials { username: if acceptor_username.is_empty() { @@ -177,8 +173,11 @@ async fn handle_browser_session( }; let mut acceptor = Acceptor::new( SecurityProtocol::SSL, - AcceptorDesktopSize { width, height }, - acceptor_capabilities(width, height), + AcceptorDesktopSize { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + }, + Vec::new(), Some(placeholder_creds), ); @@ -197,7 +196,6 @@ async fn handle_browser_session( acceptor .step(&[], &mut acc_scratch) .map_err(|e| anyhow!("acceptor step empty: {e:?}"))?; - acc_scratch.clear(); if acceptor.reached_security_upgrade().is_none() { anyhow::bail!("acceptor did not reach SecurityUpgrade after synthetic CR/CC"); @@ -205,22 +203,53 @@ async fn handle_browser_session( acceptor.mark_security_upgrade_as_done(); let client_erased: ErasedStream = Box::new(client_tcp); - let client_framed: TokioFramed = + let mut client_framed: TokioFramed = TokioFramed::new_with_leftover(client_erased, client_leftover); - debug!("rdcleanpath: running accept_finalize on client"); - let (client_final_framed, acceptor_result): (TokioFramed, AcceptorResult) = - accept_finalize(client_framed, &mut acceptor) + debug!("rdcleanpath: acceptor CredSSP"); + if acceptor.should_perform_credssp() { + ironrdp_acceptor::accept_credssp( + &mut client_framed, + &mut acceptor, + &mut ReqwestNetworkClient::new(), + ironrdp_connector::ServerName::new("infisical-rdp-bridge"), + acceptor_public_key, + None, + ) + .await + .context("acceptor: CredSSP")?; + } + info!("rdcleanpath: acceptor CredSSP complete"); + + // --- Bridge MCS/capabilities + PDUs (same as native path) --- + + let (mut client_stream, client_lo) = client_framed.into_inner(); + let (mut target_stream, target_lo) = target_framed.into_inner(); + + filter_client_mcs_connect_initial(&mut client_stream, &mut target_stream, client_lo) + .await + .context("filter client MCS Connect Initial")?; + + if !target_lo.is_empty() { + client_stream + .write_all(&target_lo) .await - .map_err(|e| anyhow!("accept_finalize: {e:?}"))?; - info!( - user_ch = acceptor_result.user_channel_id, - io_ch = acceptor_result.io_channel_id, - "rdcleanpath: client reached active stage" - ); + .context("flush target leftover to client")?; + } + + client_stream + .flush() + .await + .context("flush client stream before passthrough")?; + target_stream + .flush() + .await + .context("flush target stream before passthrough")?; debug!("rdcleanpath: bridging PDUs"); - bridge_pdus(client_final_framed, target_upgraded_framed, tx).await + let client_framed = ironrdp_tokio::TokioFramed::new(client_stream); + let target_framed = ironrdp_tokio::TokioFramed::new(target_stream); + bridge_pdus(client_framed, target_framed, tx).await } const RDCLEANPATH_READ_TIMEOUT: Duration = Duration::from_secs(30); @@ -305,10 +334,3 @@ where encode_buf(&pdu, &mut buf).map_err(|e| anyhow!("encode X.224 PDU: {e:?}"))?; Ok(buf.filled().to_vec()) } - -fn generate_throwaway_cert() -> Result> { - let subject_alt_names = vec!["localhost".to_string(), "infisical-rdp-gateway".to_string()]; - let cert = rcgen::generate_simple_self_signed(subject_alt_names) - .context("generate self-signed cert")?; - Ok(cert.cert.der().to_vec()) -} From 40741f0cdb0cef2144722de62db8bebb6a987d01 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 17:59:34 -0400 Subject: [PATCH 06/16] refactor(pam-rdp): consolidate 4 FFI start functions into 2 Make acceptor_username an optional parameter on the existing per-platform start functions instead of having separate rdcleanpath variants. NULL/empty selects native flow, non-empty selects browser RDCleanPath flow. --- packages/pam/handlers/rdp/bridge_cgo_unix.go | 39 ++--- .../pam/handlers/rdp/bridge_cgo_windows.go | 39 ++--- .../handlers/rdp/native/include/rdp_bridge.h | 37 +---- packages/pam/handlers/rdp/native/src/ffi.rs | 141 ++---------------- 4 files changed, 48 insertions(+), 208 deletions(-) diff --git a/packages/pam/handlers/rdp/bridge_cgo_unix.go b/packages/pam/handlers/rdp/bridge_cgo_unix.go index b074faf1..c09a47cc 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_unix.go +++ b/packages/pam/handlers/rdp/bridge_cgo_unix.go @@ -60,32 +60,23 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, defer C.free(unsafe.Pointer(cDomain)) } - var handle C.uint64_t - var rc C.int32_t - if acceptorUsername == "" { - rc = C.rdp_bridge_start_unix_fd( - C.int(dupFd), - cHost, - C.uint16_t(targetPort), - cUser, - cPass, - cDomain, - &handle, - ) - } else { - cAcceptor := C.CString(acceptorUsername) + var cAcceptor *C.char + if acceptorUsername != "" { + cAcceptor = C.CString(acceptorUsername) defer C.free(unsafe.Pointer(cAcceptor)) - rc = C.rdp_bridge_start_rdcleanpath_unix_fd( - C.int(dupFd), - cHost, - C.uint16_t(targetPort), - cUser, - cPass, - cDomain, - cAcceptor, - &handle, - ) } + + var handle C.uint64_t + rc := C.rdp_bridge_start_unix_fd( + C.int(dupFd), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + cDomain, + cAcceptor, + &handle, + ) if rc != C.RDP_BRIDGE_OK { return nil, fmt.Errorf("rdp bridge: start failed (status %d)", int32(rc)) } diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index 6608d430..dfdfdb01 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -60,32 +60,23 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor defer C.free(unsafe.Pointer(cDomain)) } - var handle C.uint64_t - var rc C.int32_t - if acceptorUsername == "" { - rc = C.rdp_bridge_start_windows_socket( - C.uintptr_t(dupSocket), - cHost, - C.uint16_t(targetPort), - cUser, - cPass, - cDomain, - &handle, - ) - } else { - cAcceptor := C.CString(acceptorUsername) + var cAcceptor *C.char + if acceptorUsername != "" { + cAcceptor = C.CString(acceptorUsername) defer C.free(unsafe.Pointer(cAcceptor)) - rc = C.rdp_bridge_start_rdcleanpath_windows_socket( - C.uintptr_t(dupSocket), - cHost, - C.uint16_t(targetPort), - cUser, - cPass, - cDomain, - cAcceptor, - &handle, - ) } + + var handle C.uint64_t + rc := C.rdp_bridge_start_windows_socket( + C.uintptr_t(dupSocket), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + cDomain, + cAcceptor, + &handle, + ) if rc != C.RDP_BRIDGE_OK { return nil, fmt.Errorf("rdp bridge: start failed (status %d)", int32(rc)) } diff --git a/packages/pam/handlers/rdp/native/include/rdp_bridge.h b/packages/pam/handlers/rdp/native/include/rdp_bridge.h index 9d9f39cc..e325818a 100644 --- a/packages/pam/handlers/rdp/native/include/rdp_bridge.h +++ b/packages/pam/handlers/rdp/native/include/rdp_bridge.h @@ -17,40 +17,11 @@ extern "C" { #define RDP_BRIDGE_BAD_ARG -2 #define RDP_BRIDGE_RUNTIME_ERROR -3 -// `domain` is optional. NULL or empty string means no domain (NTLM falls back -// to local-account auth). Set this for AD domain accounts so NTLM CredSSP -// authenticates against the target's AD binding rather than its local SAM. +/* `domain` and `acceptor_username` are optional (NULL or empty = unused). + * When `acceptor_username` is non-empty, the bridge runs the RDCleanPath + * (browser) flow; otherwise it runs the native RDP flow. */ #if defined(__unix__) || defined(__APPLE__) int32_t rdp_bridge_start_unix_fd( - int client_fd, - const char *target_host, - uint16_t target_port, - const char *username, - const char *password, - const char *domain, - uint64_t *out_handle -); -#endif - -#if defined(_WIN32) || defined(_WIN64) -int32_t rdp_bridge_start_windows_socket( - uintptr_t client_socket, - const char *target_host, - uint16_t target_port, - const char *username, - const char *password, - const char *domain, - uint64_t *out_handle -); -#endif - -/* Browser-flow start. Same as the native start, plus `acceptor_username`: - * the username the browser is configured to present during the acceptor's - * CredSSP exchange (decoupled from the real target username injected into - * the connector). The browser speaks RDCleanPath over the inbound stream; - * the gateway's WS upstream has already stripped framing. */ -#if defined(__unix__) || defined(__APPLE__) -int32_t rdp_bridge_start_rdcleanpath_unix_fd( int client_fd, const char *target_host, uint16_t target_port, @@ -63,7 +34,7 @@ int32_t rdp_bridge_start_rdcleanpath_unix_fd( #endif #if defined(_WIN32) || defined(_WIN64) -int32_t rdp_bridge_start_rdcleanpath_windows_socket( +int32_t rdp_bridge_start_windows_socket( uintptr_t client_socket, const char *target_host, uint16_t target_port, diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index 9a5f2d89..14026260 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -263,7 +263,9 @@ fn spawn_session( /// /// `client_fd` ownership transfers to the bridge on OK, stays with the /// caller on error. Strings must be NUL-terminated valid UTF-8. `domain` -/// may be NULL or empty for non-domain sessions. +/// and `acceptor_username` may be NULL or empty. When `acceptor_username` +/// is non-empty the bridge runs the RDCleanPath (browser) flow; otherwise +/// it runs the native RDP flow. #[cfg(unix)] #[no_mangle] pub unsafe extern "C" fn rdp_bridge_start_unix_fd( @@ -273,6 +275,7 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( username: *const c_char, password: *const c_char, domain: *const c_char, + acceptor_username: *const c_char, out_handle: *mut u64, ) -> i32 { if out_handle.is_null() { @@ -290,8 +293,11 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( Some(v) => v, None => return RDP_BRIDGE_BAD_ARG, }; - // Empty domain string is treated the same as NULL: no domain. let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty()); + let flow = match unsafe { c_str_to_owned(acceptor_username) }.filter(|s| !s.is_empty()) { + Some(acceptor_username) => SessionFlow::Rdcleanpath { acceptor_username }, + None => SessionFlow::Native, + }; use std::os::unix::io::FromRawFd; let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; @@ -303,7 +309,7 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( username, password, domain, - SessionFlow::Native, + flow, ) { Ok(id) => { unsafe { *out_handle = id }; @@ -322,125 +328,6 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( #[cfg(windows)] #[no_mangle] pub unsafe extern "C" fn rdp_bridge_start_windows_socket( - client_socket: usize, - target_host: *const c_char, - target_port: u16, - username: *const c_char, - password: *const c_char, - domain: *const c_char, - out_handle: *mut u64, -) -> i32 { - if out_handle.is_null() { - return RDP_BRIDGE_BAD_ARG; - } - let host = match unsafe { c_str_to_owned(target_host) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; - let username = match unsafe { c_str_to_owned(username) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; - let password = match unsafe { c_str_to_owned(password) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; - let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty()); - - use std::os::windows::io::{FromRawSocket, RawSocket}; - let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; - - match spawn_session( - client_tcp, - host, - target_port, - username, - password, - domain, - SessionFlow::Native, - ) { - Ok(id) => { - unsafe { *out_handle = id }; - RDP_BRIDGE_OK - } - Err(e) => { - error!(error = ?e, "rdp_bridge_start_windows_socket: failed to spawn session"); - RDP_BRIDGE_RUNTIME_ERROR - } - } -} - -/// Browser-flow start. Same shape as `rdp_bridge_start_unix_fd`, plus -/// `acceptor_username`: the username the browser is configured to present -/// during the acceptor-side CredSSP exchange (decoupled from the real -/// target username we inject into the connector). -/// -/// # Safety -/// -/// `client_fd` ownership transfers to the bridge on OK, stays with the -/// caller on error. Strings must be NUL-terminated valid UTF-8. -#[cfg(unix)] -#[no_mangle] -pub unsafe extern "C" fn rdp_bridge_start_rdcleanpath_unix_fd( - client_fd: std::ffi::c_int, - target_host: *const c_char, - target_port: u16, - username: *const c_char, - password: *const c_char, - domain: *const c_char, - acceptor_username: *const c_char, - out_handle: *mut u64, -) -> i32 { - if out_handle.is_null() { - return RDP_BRIDGE_BAD_ARG; - } - let host = match unsafe { c_str_to_owned(target_host) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; - let username = match unsafe { c_str_to_owned(username) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; - let password = match unsafe { c_str_to_owned(password) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; - let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty()); - let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { - Some(v) if !v.is_empty() => v, - _ => return RDP_BRIDGE_BAD_ARG, - }; - - use std::os::unix::io::FromRawFd; - let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; - - match spawn_session( - client_tcp, - host, - target_port, - username, - password, - domain, - SessionFlow::Rdcleanpath { acceptor_username }, - ) { - Ok(id) => { - unsafe { *out_handle = id }; - RDP_BRIDGE_OK - } - Err(e) => { - error!(error = ?e, "rdp_bridge_start_rdcleanpath_unix_fd: failed to spawn session"); - RDP_BRIDGE_RUNTIME_ERROR - } - } -} - -/// # Safety -/// -/// See `rdp_bridge_start_rdcleanpath_unix_fd`. -#[cfg(windows)] -#[no_mangle] -pub unsafe extern "C" fn rdp_bridge_start_rdcleanpath_windows_socket( client_socket: usize, target_host: *const c_char, target_port: u16, @@ -466,9 +353,9 @@ pub unsafe extern "C" fn rdp_bridge_start_rdcleanpath_windows_socket( None => return RDP_BRIDGE_BAD_ARG, }; let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty()); - let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { - Some(v) if !v.is_empty() => v, - _ => return RDP_BRIDGE_BAD_ARG, + let flow = match unsafe { c_str_to_owned(acceptor_username) }.filter(|s| !s.is_empty()) { + Some(acceptor_username) => SessionFlow::Rdcleanpath { acceptor_username }, + None => SessionFlow::Native, }; use std::os::windows::io::{FromRawSocket, RawSocket}; @@ -481,14 +368,14 @@ pub unsafe extern "C" fn rdp_bridge_start_rdcleanpath_windows_socket( username, password, domain, - SessionFlow::Rdcleanpath { acceptor_username }, + flow, ) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK } Err(e) => { - error!(error = ?e, "rdp_bridge_start_rdcleanpath_windows_socket: failed to spawn session"); + error!(error = ?e, "rdp_bridge_start_windows_socket: failed to spawn session"); RDP_BRIDGE_RUNTIME_ERROR } } From 8980609e75a1d64c0416a2a75fd010b2d43796ff Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 18:02:40 -0400 Subject: [PATCH 07/16] chore(pam-rdp): remove debug logging from bridge --- packages/pam/handlers/rdp/native/src/bridge.rs | 8 +------- .../pam/handlers/rdp/native/src/rdcleanpath.rs | 16 +--------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 48177236..87269991 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -31,7 +31,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, warn}; +use tracing::{info, warn}; use crate::cap_filter; use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; @@ -184,10 +184,6 @@ where if offset == NOT_ACTIVE && action == Action::FastPath { offset = elapsed_ns_since(started_at); recording_offset_ns.store(offset, Ordering::Relaxed); - debug!( - skip_ms = offset / 1_000_000, - "first FastPath target frame, recording starts" - ); } if offset != NOT_ACTIVE { tap_target_to_client(action, &frame, started_at, offset, &tx_t2c); @@ -375,7 +371,6 @@ fn try_filter_client_info(frame: &[u8]) -> Option> { if !cap_filter::client_info::clear_compression(user_data.slice_mut(&mut out)) { return None; } - debug!("Client Info PDU: cleared INFO_COMPRESSION + CompressionTypeMask"); Some(out) } @@ -409,7 +404,6 @@ fn try_filter_confirm_active(frame: &[u8]) -> Option> { if let Some(codecs_offset) = codecs_body_offset_in_frame { cap_filter::bitmap_codecs_cap::clear_codec_count(&mut out[codecs_offset..]); } - debug!("Confirm Active: cleared Order support + BitmapCodecs count"); Some(out) } diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 9af047b1..0b81bab5 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -15,7 +15,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::{timeout, Duration}; use tokio_util::sync::CancellationToken; -use tracing::{debug, info}; +use tracing::info; use crate::bridge::{ bridge_pdus, build_acceptor_tls_with_cert, filter_client_mcs_connect_initial, @@ -49,11 +49,9 @@ async fn handle_browser_session( info!(host = %target.host, port = target.port, "rdcleanpath: starting browser session"); let _ = rustls::crypto::ring::default_provider().install_default(); - debug!("rdcleanpath: reading RDCleanPath request from client"); let (request_pdu, client_leftover) = read_rdcleanpath_pdu(&mut client_tcp) .await .context("read RDCleanPath Request")?; - debug!("rdcleanpath: received RDCleanPath request"); let request = request_pdu .into_enum() .map_err(|e| anyhow!("RDCleanPath enum: {e}"))?; @@ -67,11 +65,9 @@ async fn handle_browser_session( }; info!(destination, "RDCleanPath: received Request"); - debug!("rdcleanpath: connecting to target"); let target_tcp = TcpStream::connect((target.host.as_str(), target.port)) .await .with_context(|| format!("connect target {}:{}", target.host, target.port))?; - debug!("rdcleanpath: target TCP connected"); let target_addr = target_tcp.local_addr().context("local_addr")?; let mut target_framed = TokioFramed::new(target_tcp); @@ -82,14 +78,11 @@ async fn handle_browser_session( let x224_cc_target = read_tpkt_pdu(&mut target_framed) .await .context("read X.224 CC")?; - debug!(len = x224_cc_target.len(), "received X.224 CC from target"); let (initial_stream, target_leftover) = target_framed.into_inner(); - debug!("rdcleanpath: TLS upgrading target"); let (upgraded_stream, target_cert) = ironrdp_tls::upgrade(initial_stream, &target.host) .await .context("TLS upgrade to target")?; - debug!("rdcleanpath: target TLS upgraded"); let (_tls_config, acceptor_public_key, throwaway_cert_der) = build_acceptor_tls_with_cert().context("build throwaway cert")?; @@ -113,10 +106,6 @@ async fn handle_browser_session( .write_all(&response_der) .await .context("write RDCleanPath Response to client")?; - debug!( - len = response_der.len(), - "rdcleanpath: sent RDCleanPath response" - ); // --- Connector: advance past X.224, then CredSSP only --- @@ -145,7 +134,6 @@ async fn handle_browser_session( let server_public_key = ironrdp_tls::extract_tls_server_public_key(&target_cert) .ok_or_else(|| anyhow!("extract target public key"))?; - debug!("rdcleanpath: connector CredSSP"); if connector.should_perform_credssp() { perform_connector_credssp( &mut connector, @@ -206,7 +194,6 @@ async fn handle_browser_session( let mut client_framed: TokioFramed = TokioFramed::new_with_leftover(client_erased, client_leftover); - debug!("rdcleanpath: acceptor CredSSP"); if acceptor.should_perform_credssp() { ironrdp_acceptor::accept_credssp( &mut client_framed, @@ -246,7 +233,6 @@ async fn handle_browser_session( .await .context("flush target stream before passthrough")?; - debug!("rdcleanpath: bridging PDUs"); let client_framed = ironrdp_tokio::TokioFramed::new(client_stream); let target_framed = ironrdp_tokio::TokioFramed::new(target_stream); bridge_pdus(client_framed, target_framed, tx).await From a031f0a790d8769ed7b9622e989bfde87daf47f5 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 18:04:59 -0400 Subject: [PATCH 08/16] fix(pam-rdp): silence unused variable warning in ffi.rs --- packages/pam/handlers/rdp/native/src/ffi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index 14026260..cfab2f3b 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -401,7 +401,7 @@ pub extern "C" fn rdp_bridge_wait(handle: u64) -> i32 { error!(handle, error = ?e, "rdp_bridge_wait: session failed"); RDP_BRIDGE_SESSION_ERROR } - Err(panic) => { + Err(_) => { error!(handle, "rdp_bridge_wait: session thread panicked"); RDP_BRIDGE_THREAD_PANIC } From ba8ce28b44082a048817ac7079c2790562dacc17 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 18:12:01 -0400 Subject: [PATCH 09/16] refactor(pam-rdp): rename MITM entry points for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run_mitm → run_mitm_native, handle_browser_session → run_mitm_rdcleanpath_inner --- packages/pam/handlers/rdp/native/src/bridge.rs | 6 +++--- packages/pam/handlers/rdp/native/src/ffi.rs | 4 ++-- packages/pam/handlers/rdp/native/src/rdcleanpath.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 87269991..23722f9c 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -56,14 +56,14 @@ pub struct TargetEndpoint { pub domain: Option, } -pub async fn run_mitm( +pub async fn run_mitm_native( client_tcp: TcpStream, target: TargetEndpoint, cancel: CancellationToken, tx: EventSender, ) -> Result<()> { tokio::select! { - result = run_mitm_inner(client_tcp, target, tx) => result, + result = run_mitm_native_inner(client_tcp, target, tx) => result, _ = cancel.cancelled() => { info!("session canceled by caller"); Ok(()) @@ -71,7 +71,7 @@ pub async fn run_mitm( } } -async fn run_mitm_inner( +async fn run_mitm_native_inner( client_tcp: TcpStream, target: TargetEndpoint, tx: EventSender, diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index cfab2f3b..0ff061ea 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -14,7 +14,7 @@ use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::{error, info}; -use crate::bridge::{run_mitm, TargetEndpoint}; +use crate::bridge::{run_mitm_native, TargetEndpoint}; use crate::events::{self, SessionEvent}; use crate::rdcleanpath::run_mitm_rdcleanpath; @@ -235,7 +235,7 @@ fn spawn_session( }; match flow { SessionFlow::Native => { - run_mitm(client, endpoint, cancel_for_thread, events_tx).await + run_mitm_native(client, endpoint, cancel_for_thread, events_tx).await } SessionFlow::Rdcleanpath { acceptor_username } => { run_mitm_rdcleanpath( diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 0b81bab5..ac00b98b 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -32,7 +32,7 @@ pub async fn run_mitm_rdcleanpath( tx: EventSender, ) -> Result<()> { tokio::select! { - result = handle_browser_session(client_tcp, target, acceptor_username, tx) => result, + result = run_mitm_rdcleanpath_inner(client_tcp, target, acceptor_username, tx) => result, _ = cancel.cancelled() => { info!("rdcleanpath session canceled by caller"); Ok(()) @@ -40,7 +40,7 @@ pub async fn run_mitm_rdcleanpath( } } -async fn handle_browser_session( +async fn run_mitm_rdcleanpath_inner( mut client_tcp: TcpStream, target: TargetEndpoint, acceptor_username: String, From 059e6efec303e7d54907c41ba99c617c7ca317f0 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 18:15:49 -0400 Subject: [PATCH 10/16] docs(pam-rdp): add comments to read_rdcleanpath_pdu_inner and read_tpkt_pdu --- packages/pam/handlers/rdp/native/src/rdcleanpath.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index ac00b98b..84df2efc 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -251,6 +251,7 @@ async fn read_rdcleanpath_pdu(tcp: &mut TcpStream) -> Result<(RDCleanPathPdu, By })? } +/// Accumulates TCP reads until a complete DER-encoded RDCleanPath PDU is detected. async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result<(RDCleanPathPdu, BytesMut)> { let mut buf = Vec::with_capacity(512); loop { @@ -284,6 +285,7 @@ async fn read_rdcleanpath_pdu_inner(tcp: &mut TcpStream) -> Result<(RDCleanPathP } } +/// Reads a single TPKT-framed PDU (4-byte header with big-endian length) from raw TCP. async fn read_tpkt_pdu(framed: &mut TokioFramed) -> Result> where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Sync + Unpin + 'static, From f795c1fec9fcbaa7b75f5d003258bd16fdb9329b Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 18:17:47 -0400 Subject: [PATCH 11/16] docs(pam-rdp): document run_mitm_rdcleanpath_inner step-by-step --- packages/pam/handlers/rdp/native/src/rdcleanpath.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 84df2efc..4a0f4ee2 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -40,6 +40,14 @@ pub async fn run_mitm_rdcleanpath( } } +/// Browser MITM flow for clients that speak RDCleanPath (IronRDP WASM). +/// +/// 1. Read RDCleanPath Request from client, extract the X.224 CR. +/// 2. Forward CR to target, read CC, TLS-upgrade the target connection. +/// 3. Build a throwaway cert, wrap a fake CC + cert in an RDCleanPath Response, send to client. +/// 4. Advance connector past X.224, run CredSSP to the target. +/// 5. Advance acceptor with synthetic X.224 CR/CC, run CredSSP to the client. +/// 6. Bridge MCS/capabilities + PDUs (shared with the native path). async fn run_mitm_rdcleanpath_inner( mut client_tcp: TcpStream, target: TargetEndpoint, From d1552c24f3d685bd76f5a00ebbb3b5bab8e1a709 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 13 May 2026 18:19:15 -0400 Subject: [PATCH 12/16] docs(pam-rdp): expand CR/CC abbreviations in rdcleanpath comment --- packages/pam/handlers/rdp/native/src/rdcleanpath.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index 4a0f4ee2..d72e3ce1 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -42,8 +42,8 @@ pub async fn run_mitm_rdcleanpath( /// Browser MITM flow for clients that speak RDCleanPath (IronRDP WASM). /// -/// 1. Read RDCleanPath Request from client, extract the X.224 CR. -/// 2. Forward CR to target, read CC, TLS-upgrade the target connection. +/// 1. Read RDCleanPath Request from client, extract the X.224 CR (Connection Request). +/// 2. Forward CR to target, read CC (Connection Confirm), TLS-upgrade the target connection. /// 3. Build a throwaway cert, wrap a fake CC + cert in an RDCleanPath Response, send to client. /// 4. Advance connector past X.224, run CredSSP to the target. /// 5. Advance acceptor with synthetic X.224 CR/CC, run CredSSP to the client. From 498dcb81a28fbc7ffaca5dd5e64bb2cb0e489518 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 15 May 2026 15:58:40 -0400 Subject: [PATCH 13/16] remove unnecessary if statement --- packages/pam/handlers/rdp/native/src/rdcleanpath.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index d72e3ce1..e7c6f3bb 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -159,11 +159,7 @@ async fn run_mitm_rdcleanpath_inner( // --- Acceptor: advance past X.224, then CredSSP only --- let placeholder_creds = AcceptorCredentials { - username: if acceptor_username.is_empty() { - "infisical".to_owned() - } else { - acceptor_username - }, + username: acceptor_username, password: "infisical".to_owned(), domain: None, }; From 6ec59075cc7a6be322640eaefeb038220d2b3e55 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 15 May 2026 16:49:06 -0400 Subject: [PATCH 14/16] split acceptor cert generation from TLS config build --- .../pam/handlers/rdp/native/src/bridge.rs | 24 ++++++++++++------- .../handlers/rdp/native/src/rdcleanpath.rs | 6 ++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 99c85f14..5e5a3cc7 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -582,8 +582,9 @@ async fn run_acceptor_half( client_tcp: TcpStream, username: String, ) -> Result<(ErasedStream, bytes::BytesMut)> { - let (server_tls, acceptor_public_key, _cert_der) = - build_acceptor_tls_with_cert().context("build acceptor TLS config")?; + let (acceptor_public_key, _cert_der, certified_key) = + generate_acceptor_cert().context("generate acceptor cert")?; + let server_tls = build_acceptor_tls_config(certified_key).context("build acceptor TLS config")?; let server_tls = Arc::new(server_tls); let acceptor_framed = ironrdp_tokio::TokioFramed::new(client_tcp); @@ -843,8 +844,7 @@ where Ok(()) } -pub(crate) fn build_acceptor_tls_with_cert( -) -> Result<(tokio_rustls::rustls::ServerConfig, Vec, Vec)> { +pub(crate) fn generate_acceptor_cert() -> Result<(Vec, Vec, rcgen::CertifiedKey)> { use x509_cert::der::Decode; let subject_alt_names = vec!["localhost".to_string(), "infisical-rdp-bridge".to_string()]; @@ -859,13 +859,19 @@ pub(crate) fn build_acceptor_tls_with_cert( .to_vec(); let cert_der_bytes = cert_der.as_ref().to_vec(); - let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into()); - let config = tokio_rustls::rustls::ServerConfig::builder() + Ok((public_key, cert_der_bytes, cert)) +} + +pub(crate) fn build_acceptor_tls_config( + certified: rcgen::CertifiedKey, +) -> Result { + let cert_der = certified.cert.der().clone(); + let key_der = + rustls::pki_types::PrivateKeyDer::Pkcs8(certified.key_pair.serialize_der().into()); + tokio_rustls::rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert_der], key_der) - .context("rustls ServerConfig")?; - - Ok((config, public_key, cert_der_bytes)) + .context("rustls ServerConfig") } pub trait AsyncReadWrite: AsyncRead + AsyncWrite {} diff --git a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs index e7c6f3bb..fac3e54f 100644 --- a/packages/pam/handlers/rdp/native/src/rdcleanpath.rs +++ b/packages/pam/handlers/rdp/native/src/rdcleanpath.rs @@ -18,7 +18,7 @@ use tokio_util::sync::CancellationToken; use tracing::info; use crate::bridge::{ - bridge_pdus, build_acceptor_tls_with_cert, filter_client_mcs_connect_initial, + bridge_pdus, filter_client_mcs_connect_initial, generate_acceptor_cert, perform_connector_credssp, ErasedStream, TargetEndpoint, }; use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; @@ -92,8 +92,8 @@ async fn run_mitm_rdcleanpath_inner( .await .context("TLS upgrade to target")?; - let (_tls_config, acceptor_public_key, throwaway_cert_der) = - build_acceptor_tls_with_cert().context("build throwaway cert")?; + let (acceptor_public_key, throwaway_cert_der, _certified_key) = + generate_acceptor_cert().context("generate throwaway cert")?; let fake_cc_bytes = encode_x224(X224(ConnectionConfirm::Response { flags: ResponseFlags::empty(), From 7ac33085e84ef9e0333ffc2c4f612be7adf280f6 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 15 May 2026 19:25:37 -0400 Subject: [PATCH 15/16] remove unused constant --- packages/pam/local/database-proxy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pam/local/database-proxy.go b/packages/pam/local/database-proxy.go index 8d4c180b..c418795c 100644 --- a/packages/pam/local/database-proxy.go +++ b/packages/pam/local/database-proxy.go @@ -27,7 +27,6 @@ type ALPN string const ( ALPNInfisicalPAMProxy ALPN = "infisical-pam-proxy" - ALPNInfisicalPAMRDPBrowser ALPN = "infisical-pam-rdp-browser" ALPNInfisicalPAMCancellation ALPN = "infisical-pam-session-cancellation" ALPNInfisicalPAMCapabilities ALPN = "infisical-pam-capabilities" ) From 8d4ee9c4ca99dd76d640de5aad85e93edb36a822 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 15 May 2026 20:12:03 -0400 Subject: [PATCH 16/16] format --- packages/pam/handlers/rdp/native/src/bridge.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 5e5a3cc7..f9dde70e 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -584,7 +584,8 @@ async fn run_acceptor_half( ) -> Result<(ErasedStream, bytes::BytesMut)> { let (acceptor_public_key, _cert_der, certified_key) = generate_acceptor_cert().context("generate acceptor cert")?; - let server_tls = build_acceptor_tls_config(certified_key).context("build acceptor TLS config")?; + let server_tls = + build_acceptor_tls_config(certified_key).context("build acceptor TLS config")?; let server_tls = Arc::new(server_tls); let acceptor_framed = ironrdp_tokio::TokioFramed::new(client_tcp);