From ab0d319891606a17aae36aad16feea99915a4eff Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Mon, 20 Apr 2026 16:13:47 -0400 Subject: [PATCH] handle SET_ERROR_INFO_PDU and add timeout in capability exchange Some misconfigured RDS Session Hosts (expired licensing grace period, Connection Broker rejection) never send DEMANDACTIVEPDU during the capability exchange. They either respond with SET_ERROR_INFO_PDU (e.g. ERRINFO_CB_CONNECTION_CANCELLED) or nothing at all. Without a sub-timeout on the MCS queue read, connect() hangs until the caller's outer timeout fires, masking the server's actual error. Adds: - A sub-timeout on the MCS queue read (half of target.timeout, minimum 1s) so a useful error is surfaced before any outer wait_for can mask it. - Detection of SET_ERROR_INFO_PDU at the capability exchange stage, matching the existing handling in __confirm_active_pdu so the server's error code and name are surfaced to the caller. Resolves #43. --- aardwolf/connection.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/aardwolf/connection.py b/aardwolf/connection.py index 6d7b971..47b87ee 100644 --- a/aardwolf/connection.py +++ b/aardwolf/connection.py @@ -759,20 +759,41 @@ async def __handle_license(self): async def __handle_mandatory_capability_exchange(self): try: - # waiting for server to demand active pdu and inside send its capabilities - data, err = await self.__joined_channels['MCS'].out_queue.get() + # Waiting for server to send DEMANDACTIVEPDU containing its capabilities. + # Some misconfigured servers (e.g. Server 2019 with expired RDS licensing + # grace period or a rejecting Connection Broker) never send the expected + # PDU and instead send SET_ERROR_INFO_PDU after a long delay (30-60s), + # or send nothing at all. Without a sub-timeout here the outer + # asyncio.wait_for on connect() fires first with a generic TimeoutError, + # masking the real server behaviour. Use half the target timeout so we + # still surface a useful error before the outer wait_for kicks in. + try: + cap_timeout = max(1, (self.target.timeout if self.target.timeout else 10) // 2) + data, err = await asyncio.wait_for(self.__joined_channels['MCS'].out_queue.get(), timeout=cap_timeout) + except asyncio.TimeoutError: + raise Exception('Server did not send DEMANDACTIVEPDU within timeout. The server may have RDS licensing or Connection Broker issues.') if err is not None: raise err data_start_offset = 0 if self.__server_connect_pdu[TS_UD_TYPE.SC_SECURITY].encryptionLevel == 1: - # encryptionLevel == 1 means that server data is not encrypted. This results in this part of the negotiation + # encryptionLevel == 1 means that server data is not encrypted. This results in this part of the negotiation # that the server sends data to the client with an empty security header (which is not documented....) data_start_offset = 4 data = data[data_start_offset:] shc = TS_SHARECONTROLHEADER.from_bytes(data) if shc.pduType != PDUTYPE.DEMANDACTIVEPDU: + # The server may send a DATAPDU with SET_ERROR_INFO_PDU instead of a + # DEMANDACTIVEPDU when it decides to reject the session (e.g. RDS + # Connection Broker cancellation, ERRINFO_CB_CONNECTION_CANCELLED). + # Surface the server's error code rather than a generic "unexpected + # reply" message so the caller knows what actually went wrong. + if shc.pduType == PDUTYPE.DATAPDU: + shd = TS_SHAREDATAHEADER.from_bytes(data) + if shd.pduType2 == PDUTYPE2.SET_ERROR_INFO_PDU: + res = TS_SET_ERROR_INFO_PDU.from_bytes(data) + raise Exception('Server replied with error during capability exchange! Code: %s ErrName: %s' % (hex(res.errorInfoRaw), res.errorInfo.name)) raise Exception('Unexpected reply! Expected DEMANDACTIVEPDU got "%s" instead!' % shc.pduType.name) res = TS_DEMAND_ACTIVE_PDU.from_bytes(data)