diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index 20aef3c6e21da..1804bd8ece4e6 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -641,8 +641,8 @@ func (c *Client) handlePNG(cb *C.CGOPNG) C.CGOErrCode { return C.ErrCodeSuccess } -//export handle_remote_fx_frame -func handle_remote_fx_frame(handle C.uintptr_t, data *C.uint8_t, length C.uint32_t) C.CGOErrCode { +//export handle_fastpath_pdu +func handle_fastpath_pdu(handle C.uintptr_t, data *C.uint8_t, length C.uint32_t) C.CGOErrCode { goData := asRustBackedSlice(data, int(length)) return cgo.Handle(handle).Value().(*Client).handleRDPFastPathPDU(goData) } @@ -660,6 +660,24 @@ func (c *Client) handleRDPFastPathPDU(data []byte) C.CGOErrCode { return C.ErrCodeSuccess } +//export handle_rdp_channel_ids +func handle_rdp_channel_ids(handle C.uintptr_t, io_channel_id C.uint16_t, user_channel_id C.uint16_t) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).handleRDPChannelIDs(io_channel_id, user_channel_id) +} + +func (c *Client) handleRDPChannelIDs(ioChannelID, userChannelID C.uint16_t) C.CGOErrCode { + c.cfg.Log.Debugf("Received RDP channel IDs: io_channel_id=%d, user_channel_id=%d", ioChannelID, userChannelID) + + if err := c.cfg.Conn.WriteMessage(tdp.RDPChannelIDs{ + IOChannelID: uint16(ioChannelID), + UserChannelID: uint16(userChannelID), + }); err != nil { + c.cfg.Log.Errorf("failed handling RDPChannelIDs: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess +} + //export handle_remote_copy func handle_remote_copy(handle C.uintptr_t, data *C.uint8_t, length C.uint32_t) C.CGOErrCode { goData := C.GoBytes(unsafe.Pointer(data), C.int(length)) diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index 7254ecc9d19a0..2d74fcb9a0234 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -214,6 +214,22 @@ impl Client { debug!("connection_result: {:?}", connection_result); + unsafe { + match handle_rdp_channel_ids( + go_ref, + connection_result.io_channel_id, + connection_result.user_channel_id, + ) { + CGOErrCode::ErrCodeSuccess => {} + _ => { + return Err(ConnectError::IronRdpError(SessionError::new( + "handle_rdp_channel_ids error", + SessionErrorKind::General, + ))); + } + }; + }; + let x224_processor = x224::Processor::new( swap_hashmap_kv(connection_result.static_channels), connection_result.user_channel_id, @@ -288,7 +304,7 @@ impl Client { ironrdp_pdu::Action::FastPath => { let go_ref = self.go_ref; match unsafe { - handle_remote_fx_frame(go_ref, frame.as_mut_ptr(), frame.len() as u32) + handle_fastpath_pdu(go_ref, frame.as_mut_ptr(), frame.len() as u32) } { CGOErrCode::ErrCodeSuccess => continue, err => { @@ -1550,7 +1566,12 @@ pub struct CGOSharedDirectoryListRequest { extern "C" { fn handle_png(client_ref: usize, b: *mut CGOPNG) -> CGOErrCode; fn handle_remote_copy(client_ref: usize, data: *mut u8, len: u32) -> CGOErrCode; - fn handle_remote_fx_frame(client_ref: usize, data: *mut u8, len: u32) -> CGOErrCode; + fn handle_fastpath_pdu(client_ref: usize, data: *mut u8, len: u32) -> CGOErrCode; + fn handle_rdp_channel_ids( + client_ref: usize, + io_channel_id: u16, + user_channel_id: u16, + ) -> CGOErrCode; fn tdp_sd_acknowledge(client_ref: usize, ack: *mut CGOSharedDirectoryAcknowledge) -> CGOErrCode; fn tdp_sd_info_request( diff --git a/lib/srv/desktop/tdp/proto.go b/lib/srv/desktop/tdp/proto.go index 0c972cc0691fa..d28054cbea019 100644 --- a/lib/srv/desktop/tdp/proto.go +++ b/lib/srv/desktop/tdp/proto.go @@ -76,6 +76,7 @@ const ( TypeNotification = MessageType(28) TypeRDPFastPathPDU = MessageType(29) TypeRDPResponsePDU = MessageType(30) + TypeRDPChannelIDs = MessageType(31) ) // Message is a Go representation of a desktop protocol message. @@ -118,6 +119,8 @@ func decodeMessage(firstByte byte, in byteReader) (Message, error) { return decodeRDPFastPathPDU(in) case TypeRDPResponsePDU: return decodeRDPResponsePDU(in) + case TypeRDPChannelIDs: + return decodeRDPChannelIDs(in) case TypeMouseMove: return decodeMouseMove(in) case TypeMouseButton: @@ -352,6 +355,30 @@ func (r RDPResponsePDU) Encode() ([]byte, error) { return buf.Bytes(), nil } +// RDPChannelIDs are the IO and user channel IDs negotiated during the RDP connection. +// +// See "3. Channel Connection" at https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/023f1e69-cfe8-4ee6-9ee0-7e759fb4e4ee +// +// | message type (31) | io_channel_id uint16 | user_channel_id uint16 | +type RDPChannelIDs struct { + IOChannelID uint16 + UserChannelID uint16 +} + +func (c RDPChannelIDs) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeRDPChannelIDs)) + writeUint16(buf, c.IOChannelID) + writeUint16(buf, c.UserChannelID) + return buf.Bytes(), nil +} + +func decodeRDPChannelIDs(in byteReader) (RDPChannelIDs, error) { + var ids RDPChannelIDs + err := binary.Read(in, binary.BigEndian, &ids) + return ids, trace.Wrap(err) +} + // MouseMove is the mouse movement message. // | message type (3) | x uint32 | y uint32 | type MouseMove struct { @@ -1520,6 +1547,12 @@ func decodeString(r io.Reader, maxLen uint32) (string, error) { return string(s), nil } +// writeUint16 writes v to b in big endian order +func writeUint16(b *bytes.Buffer, v uint16) { + b.WriteByte(byte(v >> 8)) + b.WriteByte(byte(v)) +} + // writeUint32 writes v to b in big endian order func writeUint32(b *bytes.Buffer, v uint32) { b.WriteByte(byte(v >> 24)) diff --git a/rfd/0037-desktop-access-protocol.md b/rfd/0037-desktop-access-protocol.md index daa8912aa8864..4e046bb40b23e 100644 --- a/rfd/0037-desktop-access-protocol.md +++ b/rfd/0037-desktop-access-protocol.md @@ -282,3 +282,15 @@ It is sent from TDP server to client. At the time of writing, the purpose of thi Some messages passed to the TDP client via a FastPath Frame warrant a response, which can be sent from the TDP client to the server with this message. At the time of writing this message is used to send responses to RemoteFX frames, which occasionaly demand such, but in theory it can be used to carry any raw RDP response message intended to be written directly into the TDP server-side's RDP connection. + +#### 31 - RDP Channel IDs + +``` +| message type (31) | io_channel_id uint16 | user_channel_id uint16 | +``` + +During the RDP connection sequence the client and server negotiate channel IDs for the I/O and user channels, which are used in the RemoteFX +response frames (see message type 30, above) . This message is sent by the TDP server to the TDP client so that such response frames can be +properly formulated. + +See "3. Channel Connection" at https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/023f1e69-cfe8-4ee6-9ee0-7e759fb4e4ee diff --git a/web/packages/teleport/src/ironrdp/src/lib.rs b/web/packages/teleport/src/ironrdp/src/lib.rs index 30f0a59ad2eed..7cc62edab18ea 100644 --- a/web/packages/teleport/src/ironrdp/src/lib.rs +++ b/web/packages/teleport/src/ironrdp/src/lib.rs @@ -172,11 +172,11 @@ pub struct FastPathProcessor { #[wasm_bindgen] impl FastPathProcessor { #[wasm_bindgen(constructor)] - pub fn new(width: u16, height: u16) -> Self { + pub fn new(width: u16, height: u16, io_channel_id: u16, user_channel_id: u16) -> Self { Self { fast_path_processor: IronRdpFastPathProcessorBuilder { - io_channel_id: 1003, // todo(isaiah) - user_channel_id: 1004, // todo(isaiah) + io_channel_id, + user_channel_id, } .build(), image: DecodedImage::new(PixelFormat::RgbA32, width, height), diff --git a/web/packages/teleport/src/lib/tdp/client.ts b/web/packages/teleport/src/lib/tdp/client.ts index a54f255da931b..763cba33ebec9 100644 --- a/web/packages/teleport/src/lib/tdp/client.ts +++ b/web/packages/teleport/src/lib/tdp/client.ts @@ -82,14 +82,18 @@ export default class Client extends EventEmitterWebAuthnSender { protected codec: Codec; protected socket: WebSocket | undefined; private socketAddr: string; + private width: number; + private height: number; private sdManager: SharedDirectoryManager; - private fastPathProcessor: FastPathProcessor; + private fastPathProcessor: FastPathProcessor | undefined; private logger = Logger.create('TDPClient'); constructor(socketAddr: string, width: number, height: number) { super(); this.socketAddr = socketAddr; + this.width = width; + this.height = height; this.codec = new Codec(); this.sdManager = new SharedDirectoryManager(); @@ -102,7 +106,6 @@ export default class Client extends EventEmitterWebAuthnSender { // init initializes the wasm module into memory init().then(() => { init_wasm_log(wasmLogLevel); - this.fastPathProcessor = new FastPathProcessor(width, height); }); } @@ -149,7 +152,10 @@ export default class Client extends EventEmitterWebAuthnSender { case MessageType.PNG2_FRAME: this.handlePng2Frame(buffer); break; - case MessageType.REMOTE_FX_FRAME: + case MessageType.RDP_CHANNEL_IDS: + this.handleRDPChannelIDs(buffer); + break; + case MessageType.RDP_FASTPATH_PDU: this.handleRDPFastPathPDU(buffer); break; case MessageType.CLIENT_SCREEN_SPEC: @@ -270,9 +276,28 @@ export default class Client extends EventEmitterWebAuthnSender { ); } + handleRDPChannelIDs(buffer: ArrayBuffer) { + const { ioChannelId, userChannelId } = + this.codec.decodeRDPChannelIDs(buffer); + + this.fastPathProcessor = new FastPathProcessor( + this.width, + this.height, + ioChannelId, + userChannelId + ); + } + handleRDPFastPathPDU(buffer: ArrayBuffer) { let rdpFastPathPDU = this.codec.decodeRDPFastPathPDU(buffer); + // This should never happen but let's catch it with an error in case it does. + if (!this.fastPathProcessor) + this.handleError( + new Error('FastPathProcessor not initialized'), + TdpClientEvent.CLIENT_ERROR + ); + this.fastPathProcessor.process( rdpFastPathPDU, this, diff --git a/web/packages/teleport/src/lib/tdp/codec.ts b/web/packages/teleport/src/lib/tdp/codec.ts index 654094d0bc646..009a1b383f43d 100644 --- a/web/packages/teleport/src/lib/tdp/codec.ts +++ b/web/packages/teleport/src/lib/tdp/codec.ts @@ -58,8 +58,9 @@ export enum MessageType { SHARED_DIRECTORY_LIST_RESPONSE = 26, PNG2_FRAME = 27, NOTIFICATION = 28, - REMOTE_FX_FRAME = 29, - RESPONSE_FRAME = 30, + RDP_FASTPATH_PDU = 29, + RDP_RESPONSE_PDU = 30, + RDP_CHANNEL_IDS = 31, __LAST, // utility value } @@ -101,6 +102,12 @@ export type ClipboardData = { data: string; }; +// | message type (31) | io_channel_id uint16 | user_channel_id uint16 | +export type RDPChannelIDs = { + ioChannelId: number; + userChannelId: number; +}; + export enum Severity { Info = 0, Warning = 1, @@ -808,7 +815,7 @@ export default class Codec { const view = new DataView(buffer); let offset = 0; - view.setUint8(offset, MessageType.RESPONSE_FRAME); + view.setUint8(offset, MessageType.RDP_RESPONSE_PDU); offset += byteLength; view.setUint32(offset, responseFrame.byteLength); offset += uint32Length; @@ -949,6 +956,17 @@ export default class Codec { return new RDPFastPathPDU(new Uint8Array(data)); } + // | message type (31) | io_channel_id uint16 | user_channel_id uint16 | + decodeRDPChannelIDs(buffer: ArrayBuffer): RDPChannelIDs { + const dv = new DataView(buffer); + let offset = 0; + offset += byteLength; // eat message type + const ioChannelId = dv.getUint16(offset); + offset += uint16Length; // eat io_channel_id + const userChannelId = dv.getUint16(offset); + return { ioChannelId, userChannelId }; + } + // | message type (12) | err_code error | directory_id uint32 | decodeSharedDirectoryAcknowledge( buffer: ArrayBuffer @@ -1143,5 +1161,6 @@ export default class Codec { } const byteLength = 1; +const uint16Length = 2; const uint32Length = 4; const uint64Length = uint32Length * 2;