手把手教你写一个rdp的静态虚拟通道

最近在开发基于rdp协议的组件,因为需要用到rdp的虚拟通道拓展,前后折腾了好几天,这里记录一下顺带给后面做这部分的小伙伴一个参考。

什么是静态虚拟通道

虚拟通道的能力拓展是在RDP协议的协商阶段进行announce的,这部分在BCGR那一部分文档介绍的比较清楚、MCS就是Multi-Channel Support,RDP协议有许多通道拓展,例如RDPDR、RDPEI、RDPCLIRDR等,MCS通过使用多通道(channels)来实现,每个通道可以处理特定类型的数据传输。虚拟动态通道可以随时创建和销毁,并且静态虚拟通道是依赖于静态通道来进行建立的。

另外RDP协议也留给了开发者一些接口、这样我们就能开发自己的拓展通道了。

配置dll注册表

我们需要自己开发一个dll,然后将它配置到注册表中,这样TS Service在启动MSTSC的时候就能找到我们的拓展了。

在注册表中,将子项添加到以下位置之一:

HKEY_CURRENT_USER \ Software \ Microsoft \ Terminal Server Client \ Default \ Addins

HKEY_CURRENT_USER \ Software \ Microsoft \ Terminal Server Client \ connection \ Addins

例如,我们需要开发的通道拓展是"rdpgw"(需要注意的是通道名称建议用英文,并且字符串长度不要超过7),那么我们的配置的目录项就是

HKEY_CURRENT_USER \ Software \ Microsoft \ Terminal Server Client \ Default \ Addins\Default\RDPGW,如果你的机器不存在这个目录的话,就自己创建。

\Default\Addins项下的条目适用于所有连接。\connection\Addins键下的条目仅适用于由connection标识的连接。可以使用连接管理器创建和管理连接。

可以为子项指定任何名称。它必须包含REG_SZREG_EXPAND_SZ值,并且可以选择包含REG_DWORD值。REG_SZREG_EXPAND_SZ值的语法如下。

Name = DLLname

如果NameREG_EXPAND_SZ值,则它可以包含在运行时扩展的未扩展环境变量。

DLLname的值可以是完全限定路径。如果DLLname不包含路径,则使用标准 DLL 搜索策略。

例如:

编写DLL

需要注意这个DLL一定要抛出去一个函数BOOL VCAPITYPE VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints),一个字都不能差,因为这个是TS服务调用你的DLL的入口函数签名。至于其中使用的一些API,可以查看下面表格中的文档。

功能描述
虚拟通道初始化注册客户端要使用的虚拟通道的名称,并提供VirtualChannelInitEvent回调函数,远程桌面服务通过该函数通知客户端有关影响客户端连接的事件。
虚拟频道开放打开指定虚拟通道的客户端,并提供VirtualChannelOpenEvent回调函数,远程桌面服务通过该回调函数通知客户端有关影响虚拟通道的事件。
虚拟通道写入将数据写入虚拟通道。远程桌面服务将此数据发送到虚拟通道的服务器端。服务器端调用WTSVrtualChannelRead函数读取数据。
虚拟通道关闭关闭虚拟通道。

这是作者写的demo,至于日志框架使用的是spdlog,这个无关紧要。这里面许多方法都是异步的,所以调用的时候一定要注意如果你使用了堆分配的内存时、不要在他函数返回后立刻释放,一定要在对应的回调事件中处理。

需要注意的是这里一定要声明一个全局变量来保存pEntryPoints,因为这个方法是异步的,VirtualChannelEntry函数返回后,该指针不再有效 。并且一定要在VirtualChannelEntry里面调用VirtualChannelInit方法,因为这个API规定必须在这个方法里面调用,其他地方调用都会返回失败。

注意channel配置了一个选项是CHANNEL_OPTION_SHOW_PROTOCOL、它决定了数据通过 VirtualChannelWrite 函数传递给服务器端插件时的呈现方式。如果设置了这个选项,服务器端插件会看到完整的虚拟通道协议,包括协议中的 CHANNEL_PDU_HEADER,如果未设置这个选项,服务器端插件只会看到通过 VirtualChannelWrite 传递的数据部分。CHANNEL_PDU_HEADER 是虚拟通道协议中的一个结构,用于描述传输的数据的头部信息。如果选项被设置,服务器端插件能够看到这个头部信息,否则只能看到实际传递的数据,而不包括头部信息。

我们先定义一些全局变量

constexpr auto CHANNEL_NAME = "rdpgw";
​
//store pEntryPoints
PCHANNEL_ENTRY_POINTS Global_Entry_Points = NULL;
LPVOID RdpgwInitHandle = NULL;
DWORD RdpgwHandle = 0;
​
//buffer
BYTE* RdpgwRecieveBuff = NULL;
DWORD RecieveBuffLen = 0;
DWORD RecieveBuffSize = 0;
VirtualChannelEntry
VIRTUALCHANNELENTRY Virtualchannelentry;
​
BOOL VCAPITYPE Virtualchannelentry(
  [in] PCHANNEL_ENTRY_POINTS pEntryPoints
)
{...}
//https://learn.microsoft.com/en-us/windows/win32/api/cchannel/nc-cchannel-virtualchannelentry
//Pointer to a CHANNEL_ENTRY_POINTS structure that contains pointers to the client-side virtual channel functions.
//This pointer is no longer valid after the VirtualChannelEntry function returns. You must make a copy of this 
//structure in extension-allocated memory for later use.
BOOL VCAPITYPE VirtualChannelEntry(PCHANNEL_ENTRY_POINTS pEntryPoints) {
    BOOL dwReesult = FALSE;
    CHANNEL_DEF channel = {0};
    DWORD status = 0;
    size_t entryPointsSize = 0;
    std::string homePath = "C:\\Users\\Administrator\\Desktop\\rdpgw.log";
​
    logger = spdlog::basic_logger_mt("rdpgw_logger", homePath);
    logger->set_level(spdlog::level::debug);
    logger->flush_on(spdlog::level::debug);
    logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%L%$] [thread %t] %v");
​
    if (pEntryPoints == NULL) {
        logger->error("internal error,pEntryPoints is null!");
        goto end;
    }
    entryPointsSize = pEntryPoints->cbSize;
​
    logger->info("VirtualChannelEntry loaded");
    Global_Entry_Points = (PCHANNEL_ENTRY_POINTS)malloc(entryPointsSize);
    if (Global_Entry_Points == NULL) {
        logger->error("internal error,malloc error{}",GetLastError());
        goto end;
    }
    memcpy(Global_Entry_Points, pEntryPoints, entryPointsSize);
    
    //check channle name and copy that to channle's name
    if (strlen(CHANNEL_NAME) > CHANNEL_NAME_LEN)
    {
        logger->error("CHANNEL_NAME exceed max len of {}", CHANNEL_NAME_LEN);
        goto end;
    }
    memcpy(channel.name, CHANNEL_NAME, strlen(CHANNEL_NAME) + 1);
    channel.options = CHANNEL_OPTION_SHOW_PROTOCOL;
    
    //Specifies the level of virtual channel support. Set this parameter to VIRTUAL_CHANNEL_VERSION_WIN2000.
    //Your VirtualChannelEntry implementation must call the VirtualChannelInit function to initialize access to virtual channels.
    status = Global_Entry_Points->pVirtualChannelInit(&RdpgwInitHandle, &channel, 1,
        VIRTUAL_CHANNEL_VERSION_WIN2000,RdpwgChannelInitEventFunc);
    if (status != CHANNEL_RC_OK)
    {
        logger->info("VirtualChannelInit error {}",status);
        goto end;
    }
​
    dwReesult = TRUE;
    logger->debug("VirtualChannelInit finish");
end:
    return dwReesult;
}

RdpwgChannelInitEventFunc

这个方法会在VirtualChannelEntry函数返回后,客户端开始初始化时被调用,初始化客户端 DLL 对远程桌面服务虚拟通道的访问。客户端调用VirtualChannelInit来注册其虚拟通道的名称。看一下他的函数签名

VIRTUALCHANNELINIT Virtualchannelinit;
​
UINT VCAPITYPE Virtualchannelinit(
  [in]      LPVOID *ppInitHandle,
  [in, out] PCHANNEL_DEF pChannel,
  [in]      INT channelCount,
  [in]      ULONG versionRequested,
  [in]      PCHANNEL_INIT_EVENT_FN pChannelInitEventProc
)

ppInitHandle:指向接收标识客户端连接的句柄的变量的指针。使用此句柄在后续调用VirtualChannelOpen函数时识别客户端 。

pChannel:指向CHANNEL_DEF结构数组的指针 。每个结构都包含客户端 DLL 将打开的虚拟通道的名称和初始化选项。请注意,VirtualChannelInit调用不会打开这些虚拟通道;它仅保留供该应用程序使用的名称。

channelCount:指定pChannel数组中的条目数。

versionRequested:虚拟通道版本、固定为VIRTUAL_CHANNEL_VERSION_WIN2000

pChannelInitEventProc:指向应用程序定义的 VirtualChannelInitEvent函数的指针,远程桌面服务调用该函数来通知客户端 DLL 虚拟通道事件。

pChannelInitEventProc是一个函数指针,在TS Client对于数据包发送等一些事件时会被调用。看一下代码

VOID VCAPITYPE RdpwgChannelInitEventFunc(LPVOID pInitHandle, UINT event, LPVOID pData, UINT dataLength) {
    DWORD status;
       
    switch (event) {
        case CHANNEL_EVENT_CONNECTED:
        {
            status = Global_Entry_Points->pVirtualChannelOpen(RdpgwInitHandle, &RdpgwHandle, (PCHAR)CHANNEL_NAME, RdpwgChannelOpenEventFunc);
            if (status != CHANNEL_RC_OK)
            {
                logger->error("error open rdpgw channel:{}", status);
            }
            break;
        }
        case CHANNEL_EVENT_DISCONNECTED:
        {
            logger->info("CHANNEL_EVENT_DISCONNECTED");
            break;
        }
        case CHANNEL_EVENT_TERMINATED:
        {
            logger->info("CHANNEL_EVENT_TERMINATED");
            Global_Entry_Points->pVirtualChannelClose(RdpgwHandle);
            if(Global_Entry_Points) free(Global_Entry_Points);
            if(RdpgwRecieveBuff) free(RdpgwRecieveBuff);
            break;
        }
    }
}

我们在CHANNEL_EVENT_CONNECTED时向server端请求打开这个虚拟通道,注意只有在你调用pVirtualChannelOpen时,server端才能收到TS Client发送的域建立请求。至于什么时候向server发送消息,在通道打开之后就可以做这件事情了

            const char* fristData = "Hello server!";
            status = Global_Entry_Points->pVirtualChannelWrite(RdpgwHandle, LPVOID(fristData), strlen(fristData)+1, NULL);
            if (status != CHANNEL_RC_OK)
            {
                logger->error("error send first msg to server");
                break;
            }
​
            logger->info("have sent the first msg to server {}", fristData);

不过相信聪明的你一定已经发现了VirtualChannelOpen中又存在一个函数指针RdpwgChannelOpenEventFunc。没错,他就是你后面处理数据包的关键回调了,接下来我们会讲。

RdpwgChannelOpenEventFunc

先看一下函数签名

CHANNEL_OPEN_EVENT_FN ChannelOpenEventFn;
​
VOID VCAPITYPE ChannelOpenEventFn(
  [in] DWORD openHandle,
  [in] UINT event,
  [in] LPVOID pData,
  [in] UINT32 dataLength,
  [in] UINT32 totalLength,
  [in] UINT32 dataFlags
)
{...}

这些参数比较好理解,openHandle其实就是一开始我们保存的句柄,类型时DWORD。event是指示引起通知的事件。该参数可以是以下值之一。CHANNEL_EVENT_DATA_RECEIVED、CHANNEL_EVENT_WRITE_CANCELLED、CHANNEL_EVENT_WRITE_COMPLETE。

其中CHANNEL_EVENT_DATA_RECEIVED中dataFlags又有四种不同的类型:

CHANNEL_FLAG_FIRST chunk 是单个写操作写入的数据的开始。

CHANNEL_FLAG_LAST chunk是单次写操作写入的数据的结尾。

CHANNEL_FLAG_MIDDLE 这是默认设置。该块位于由单个写操作写入的数据块的中间。

CHANNEL_FLAG_ONLY 该块包含来自单个写入操作的所有数据。

其实就是指示你现在数据接受/发送到哪一步了,值得注意的是文档里面特意说明了一定要使用位运算来比较。

        if (totalLength > RecieveBuffLen)
        {
            BYTE* RdpgwRecieveBuffTmp = (BYTE*)realloc(RdpgwRecieveBuff, totalLength);
            if (!RdpgwRecieveBuffTmp)
            {
                logger->error("realloc error {}", GetLastError());
                return;
            }
            RdpgwRecieveBuff = RdpgwRecieveBuffTmp;
            RecieveBuffLen = totalLength;
        }
​
        memcpy_s(RdpgwRecieveBuff + RecieveBuffSize, RecieveBuffLen - RecieveBuffSize, pData, dataLength);
        RecieveBuffSize += dataLength;
​
        //CHANNEL_FLAG_FIRST || CHANNEL_FLAG_MIDDLE
        if ((dataFlags & CHANNEL_FLAG_FIRST)|| !(dataFlags & CHANNEL_FLAG_FIRST) && !(dataFlags & CHANNEL_FLAG_LAST))
        {
            //do nothing
            break;
        }
​
        //CHANNEL_FLAG_LAST || CHANNEL_FLAG_ONLY
        if ((dataFlags & CHANNEL_FLAG_LAST) || (dataFlags & CHANNEL_FLAG_ONLY))
        {
            //write back to server
            status = Global_Entry_Points->pVirtualChannelWrite(RdpgwHandle, RdpgwRecieveBuff, RecieveBuffSize, NULL);
            if (status!= CHANNEL_RC_OK)
            {
                logger->error("write back to server failed {}", status);
            }
​
            logger->info("write {} bytes back to server", RecieveBuffSize);
            //clear buffer
            memset(RdpgwRecieveBuff, 0,RecieveBuffLen);
            RecieveBuffSize = 0;
        }

收尾

然后我们把这份代码编译成dll,然后启动mstsc

可以看到mstsc已经load了我们的DLL。

看一下client DLL的日志:

再看一下server的日志

可以看到我们自己写的拓展通道rdpgw已经建立成功了,并且收到了客户端的消息。现在就可以愉快的进行数据传输了!

参考文档:

Virtual Channel Client Registration - Win32 apps | Microsoft Learn

VIRTUALCHANNELENTRY (cchannel.h) - Win32 apps | Microsoft Learn

https://github.com/MicrosoftDocs/win32/blob/docs/desktop-src/TermServ/virtual-channel-client-dll.md

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值