Windows 对 RDP 会话报告哪个地址?
WTS API 会为一个 RDP 会话返回报告的客户端地址,而注册表不会。请查询正确的会话 ID,并正确解码 IPv4 或 IPv6。
RDP 会话需要客户端地址,而看似显而易见的来源彼此矛盾。CLIENTNAME 给出的是计算机名,注册表保存的是其他会话的值,套接字的对端可能是网关或 NAT 地址而不是客户端。Windows 确实会按会话公开一个报告的客户端地址,但你必须查询正确的会话并正确解码响应。
把查询固定在一个会话上
从会话开始,而不是从人开始。一台 Windows RDP 主机可以为同一个账户承载多个会话,而重连之前看到的地址可能已不再对应活动链路。请把会话 ID、用户和观察时间一起记录下来。会话 ID 是 API 期望的键,也正是这个值让两个会话不至于混淆。
在主机上,已安装的会话工具会列出存在哪些会话;只有在你拥有权限时才使用管理视图。在查询之前立刻确认所选 ID,因为被回收的 ID 会返回另一个会话的数据。
qwinsta
为该 ID 调用 WTSQuerySessionInformationW
终端服务 API 用信息类 14 回答这个问题,即 WTSClientAddress。在 Windows 上一个小巧的 ctypes 包装就足够了,而且它避免了读取其他用户的配置单元。下面的代码接受显式会话 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 从偏移零开始。未指定地址意味着客户端没有报告地址,这与空字符串并不相同。远程桌面网关连接被记录为地址不可用的情况,因此经由网关的会话不会报告任何关于客户端真实地址的有用信息。此时关联工作必须转向网关自身的记录。
调试时把地址族和地址一起打印出来,因为两者不匹配通常是看似合理却错误的值的原因。在 finally 块中用 WTSFreeMemory 释放缓冲区,以免异常导致泄漏。
把报告的地址与证据分开
报告的客户端地址是客户端提供的声明,而不是可信的观测。它不是 TCP 对端,不是 VPN 网关的租约表,也不是客户端的公网地址。当真正的问题是某条 VPN 租约属于谁时,请同时关联 VPN 服务器的已认证会话记录。当问题是哪个来源到达了监听端时,请读取连接表或获准的服务器端日志。
在任何支持工具中都把这些值分开标注。缺失的 WTS 结果应当保持缺失,而不是退回到另一个会话的任意注册表值。把会话 ID 记录在地址旁边,因为同一个用户可能重新连接并获得新的值。客户端名称和报告的地址都不足以作为授予访问权限的证据。