获取远程桌面客户端的 public IP 地址

Getting public IP address of a remote's desktop client

我正在开发一个在远程服务器(Windows Server 2016-2019,Windows 10)上运行的应用程序,当用户启动与 RDP 的远程连接时。我正在使用 C++ 和 Win API.

我正在尝试获取远程桌面客户端的 public IP 地址。 我使用方法 WTSQuerySessionInformationW 并将 WTSInfoClass 设置为 WTSClientAddress。不幸的是,它看起来是客户端计算机的 returns 本地 ip 函数,例如 192.168.1.10 而不是 public。

场景是客户端从世界任何地方访问远程桌面(因此不仅仅是从本地网络)。 在应用程序和服务日志下的 Window 事件查看器 -> Microsoft -> Windows -> TerminalServices-LocalSessionManager 我可以看到 public ip 地址(源网络地址)。

我可以使用哪个函数或机制来获取此 ip 地址?

当我们读到 WTS_CLIENT_ADDRESS 结构时

The client network address is reported by the RDP client itself when it connects to the server. This could be different than the address that actually connected to the server. For example, suppose there is a NAT between the client and the server. The client can report its own IP address, but the IP address that actually connects to the server is the NAT address. For VPN connections, the IP address might not be discoverable by the client. If it cannot be discovered, the client can report the only IP address it has, which may be the ISP assigned address. Because the address may not be the actual network address, it should not be used as a form of client authentication.

为了从服务器视图中获取实际网络地址,我们可以使用 WinStationQueryInformationW with WinStationRemoteAddress- it return WINSTATIONREMOTEADDRESS

您可以复制粘贴此声明或全部 winsta.h(由于未知原因未包含在 sdk 中)

乍一看,我们可以决定 WINSTATIONREMOTEADDRESS is the same as SOCKADDR (SOCKADDR_IN and SOCKADDR_IN6) by layout. and we can do reinterpret cast pointer from WINSTATIONREMOTEADDRESS to SOCKADDR

但这是严重错误。结构有不同的对齐方式 !

C_ASSERT(FIELD_OFFSET(SOCKADDR_IN, sin_port) == 2);
C_ASSERT(FIELD_OFFSET(WINSTATIONREMOTEADDRESS, ipv4.sin_port) == 4);
C_ASSERT(FIELD_OFFSET(SOCKADDR_IN, sin_addr) == 4);
C_ASSERT(FIELD_OFFSET(WINSTATIONREMOTEADDRESS, ipv4.in_addr) == 8);

使用 WinStationQueryInformationW 需要 link 与 winsta.lib 或在运行时从 winsta.dll 获取

所以最终代码可以是下一个:

typedef enum _WINSTATIONINFOCLASS {
    // ...
    WinStationRemoteAddress = 29,
    // ...
} WINSTATIONINFOCLASS;

#define LOGONID_CURRENT     ((ULONG)-1)

typedef struct {
    unsigned short sin_family;
    union {
        struct {
            USHORT sin_port;
            ULONG in_addr;
            UCHAR sin_zero[8];
        } ipv4;
        struct {
            USHORT sin6_port;
            ULONG sin6_flowinfo;
            USHORT sin6_addr[8];
            ULONG sin6_scope_id;
        } ipv6;
    };
} WINSTATIONREMOTEADDRESS,
*PWINSTATIONREMOTEADDRESS;

EXTERN_C
DECLSPEC_IMPORT
BOOLEAN
WINAPI
WinStationQueryInformationW(
                            _In_opt_ HANDLE hServer,
                            _In_ ULONG SessionId,
                            _In_ WINSTATIONINFOCLASS WinStationInformationClass,
                            _Out_writes_bytes_(WinStationInformationLength) PVOID pWinStationInformation,
                            _In_ ULONG WinStationInformationLength,
                            _Out_ PULONG pReturnLength
                            );

ULONG GetRdpClientAddressFromServerView()
{
    ULONG dwError = NOERROR;
    ULONG cb;

    union {
        SOCKADDR sa;
        SOCKADDR_IN sa4;
        SOCKADDR_IN6 sa6;
    };

    WINSTATIONREMOTEADDRESS ra;

    if (WinStationQueryInformationW(0, LOGONID_CURRENT, WinStationRemoteAddress, &ra, sizeof(ra), &cb))
    {
        switch (sa.sa_family = ra.sin_family)
        {
        case AF_INET:
            sa4.sin_port = ra.ipv4.sin_port;
            sa4.sin_addr.S_un.S_addr = ra.ipv4.in_addr;
            RtlZeroMemory(sa4.sin_zero, sizeof(sa4.sin_zero));
            cb = sizeof(SOCKADDR_IN);
            break;
        case AF_INET6:
            sa6.sin6_port = ra.ipv6.sin6_port;
            sa6.sin6_flowinfo = ra.ipv6.sin6_flowinfo;
            memcpy(&sa6.sin6_addr, &ra.ipv6.sin6_addr, sizeof(in6_addr));
            sa6.sin6_scope_id = ra.ipv6.sin6_scope_id;
            cb = sizeof(SOCKADDR_IN6);
            break;
        default:
            dwError = ERROR_GEN_FAILURE;
        }

        if (dwError == NOERROR)
        {
            // assume that WSAStartup already called
            // WSADATA wd;
            // WSAStartup(WINSOCK_VERSION, &wd);

            char AddressString[64];
            ULONG dwAddressStringLength = _countof(AddressString);
            if (WSAAddressToStringA(&sa, cb, 0, AddressString, &dwAddressStringLength) == NOERROR)
            {
                DbgPrint("client ip is %s\n", AddressString);
            }
            else
            {
                dwError = WSAGetLastError();
            }
        }
    }
    else
    {
        dwError = GetLastError();
    }

    return dwError;
}

如果您正在为 .NET 编程,则可以使用 cassia Nuget 包。 https://github.com/danports/cassia

它提供了一个API来访问远程桌面相关信息。