Сеансу RDP нужен адрес клиента, и очевидные источники расходятся. CLIENTNAME даёт имя компьютера, реестр хранит значения из других сеансов, а противоположная сторона сокета может быть адресом шлюза или NAT, а не клиента. Windows действительно предоставляет сообщённый адрес клиента для каждого сеанса, но нужно запросить правильный сеанс и корректно декодировать ответ.

Привяжите поиск к одному сеансу

Начинайте с сеанса, а не с человека. Хост RDP под Windows может нести несколько сеансов для одной учётной записи, и адрес, замеченный до переподключения, может уже не соответствовать активному соединению. Записывайте ID сеанса, пользователя и время наблюдения вместе. ID сеанса — это ключ, которого ожидает API, и именно это значение не даёт перепутать два сеанса.

На хосте установленные средства работы с сеансами перечисляют то, что существует; используйте административное представление только когда у вас есть на это право. Подтвердите выбранный ID непосредственно перед запросом, потому что повторно использованный ID возвращает данные другого сеанса.

qwinsta

Вызовите WTSQuerySessionInformationW для этого ID

API служб терминалов отвечает на вопрос классом информации 14, WTSClientAddress. Небольшой обёртки ctypes под Windows достаточно, и она позволяет не читать куст другого пользователя. Код ниже принимает явный ID сеанса и опрашивает локальный хост.

import ctypes
import ipaddress
import socket
from ctypes import wintypes

class WTS_CLIENT_ADDRESS(ctypes.Structure):
    _fields_ = [("AddressFamily", wintypes.DWORD),
                ("Address", ctypes.c_ubyte * 20)]

wts = ctypes.WinDLL("Wtsapi32", use_last_error=True)
query = wts.WTSQuerySessionInformationW
query.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.c_int,
                  ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(wintypes.DWORD)]
query.restype = wintypes.BOOL
free = wts.WTSFreeMemory
free.argtypes = [ctypes.c_void_p]

def client_address(session_id):
    buf = ctypes.c_void_p()
    size = wintypes.DWORD()
    if not query(None, session_id, 14, ctypes.byref(buf), ctypes.byref(size)):
        raise ctypes.WinError(ctypes.get_last_error())
    try:
        if size.value < ctypes.sizeof(WTS_CLIENT_ADDRESS):
            raise RuntimeError("short WTS buffer")
        value = ctypes.cast(buf, ctypes.POINTER(WTS_CLIENT_ADDRESS)).contents
        raw = bytes(value.Address)
        if value.AddressFamily == socket.AF_INET:
            addr = ipaddress.ip_address(raw[2:6])
        elif value.AddressFamily == socket.AF_INET6:
            addr = ipaddress.ip_address(raw[:16])
        else:
            return None
        return None if addr.is_unspecified else str(addr)
    finally:
        free(buf)

Передайте целочисленный ID сеанса, который вы только что проверили, и сохраните нулевой дескриптор сервера для локальной машины. Неудачный вызов считайте ошибкой, а не отсутствующим адресом, потому что эти два случая ведут к разным выводам. Запрос сеанса другого пользователя требует соответствующего разрешения Query Information, поэтому запускайте средство под учётной записью, которой оно выдано, вместо разового повышения прав. Возвращённый буфер принадлежит API и должен освобождаться даже при неудачном декодировании.

Декодируйте семейство, затем освободите буфер

Структура — не строка IP. Её поле семейства определяет, как читать двадцать байт адреса: IPv4 начинается со смещения два, а IPv6 — со смещения ноль. Неопределённый адрес означает, что клиент его не сообщил, и это не то же самое, что пустая строка. Подключения через Remote Desktop Gateway документированы как случай, когда адрес недоступен, поэтому сеанс через шлюз не сообщает ничего полезного о реальном адресе клиента. Тогда сопоставление должно перейти к собственным записям шлюза.

При отладке печатайте семейство рядом с адресом, потому что несоответствие между ними — обычная причина правдоподобного, но неверного значения. Освободите буфер с помощью WTSFreeMemory в блоке finally, чтобы исключение его не утекло.

Отделите сообщённый адрес от доказательства

Сообщённый адрес клиента — это заявление, которое предоставляет клиент, а не надёжное наблюдение. Это не противоположная сторона TCP, не таблица аренды VPN-шлюза и не публичный адрес клиента. Когда настоящий вопрос в том, какая аренда VPN принадлежит человеку, сопоставьте аутентифицированную запись сеанса VPN-сервера одновременно. Когда вопрос в том, какой источник достиг слушателя, читайте таблицу соединений или разрешённые журналы на стороне сервера.

Держите эти значения отдельно помеченными в любом средстве поддержки. Отсутствующий результат WTS должен оставаться отсутствующим, а не подменяться произвольным значением реестра из другого сеанса. Записывайте ID сеанса рядом с адресом, потому что один и тот же пользователь может переподключиться и получить новое значение. Ни имя клиента, ни сообщённый адрес не являются достаточным доказательством для предоставления доступа.

Похожие статьи