一、编译、运行、调试
1.1 编译
对设置做两个修改,一是确保WITH_SERVER是ON,二是增加CMAKE_WINDOWS_VERSION,值至少WIN8以上。
确保WITH_SERVER是ON。WITH_SERVER决定生成的FreeRDP.sln是否要包含server相关模块,对要编译server的自然要设为ON。有两个方法,一是打开ConfigOptions.cmake,把定义WITH_SERVER初始值的“option”改为“ON”。二是运行CMake(cmake-gui)时,执行过“Configure”后,变量区找到“WITH_SERVER”,打勾。
<FreeRDP>/cmake/ConfigOptions.cmake option(WITH_SERVER "Build server binaries" OFF) 改为 option(WITH_SERVER "Build server binaries" ON)
增加CMAKE_WINDOWS_VERSION,值至少WIN8以上。运行CMake(cmake-gui),在Configure前单击“Add Entry”。

<FreeRDP>/CMakeLists.txt if(NOT DEFINED CMAKE_WINDOWS_VERSION) set(CMAKE_WINDOWS_VERSION "WIN7") endif() if(CMAKE_WINDOWS_VERSION STREQUAL "WINXP") add_definitions(-DWINVER=0x0501 -D_WIN32_WINNT=0x0501) elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN7") add_definitions(-DWINVER=0x0601 -D_WIN32_WINNT=0x0601) elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN8") add_definitions(-DWINVER=0x0602 -D_WIN32_WINNT=0x0602) elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN10") add_definitions(-DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00) endif()
CMAKE_WINDOWS_VERSION决定了预定义宏_WIN32_WINNT是什么值,要是不预先设置CMAKE_WINDOWS_VERSION,值将是0x0601,此时抓屏将使用“Mirage Driver”技术。
<FreeRDP>/server/Windows/wf_interface.h, #if _WIN32_WINNT >= 0x0602 #define WITH_DXGI_1_2 1 #endif
Win8或以上系统已不再支持“Mirage Driver”,用它抓屏会出异常,必须使用DXGI,它们区别可参考“windows远程桌面实现之一 (抓屏技术总览 MirrorDriver,DXGI,GDI) ”。
1.2 运行
TLS握手阶段,server需向client发送证书、私钥。它们从哪来?FreeRDP自带了这两个文件:server.crt、server.key。
<FreeRDP>/server/Windows/wf_peer.c static BOOL wf_peer_read_settings(freerdp_peer* client) { client->settings->CertificateFile = _strdup("server.crt"); ... client->settings->PrivateKeyFile = _strdup("server.key"); } 改为 client->settings->CertificateFile = _strdup("c:/ddksample/server.crt"); client->settings->PrivateKeyFile = _strdup("c:/ddksample/server.key");
由于代码中用的是相对路径,如果不修改源码,server可能找不到这两个文件,修改方法正如上面示例的用绝对路径代替相对路径。
1.3 调试
编译出wfreerdp-server.exe,能成功运行,但断点无法进入wfreerdp-server.dll。举个例子,wfreerdp_server_start是wfreerdp-server.dll内的一个输出函数,但断点无法进入。原因是先后有两个工程会生成wfreerdp-server.lib,第一个是wfreerdp-server.dll,第二个是wfreerdp-server.exe。由于两个同名,后面exe生成的lib会覆盖掉第一个,导致wfreerdp-server.lib调试信息丢失,断点无法进入它内中函数。
以下方法只是应付性解决这个问题,修改的人需要知道在做什么。打开“wfreerdp-server”,修改三处。
- General/Target Name: wfreerdp-server ==> wfreerdp-server-last
- General/Configuration Type: Dynamic_Library(.dll) ==> Static library(.lib)
- Advanced/Target File Extension: .dll ==> $(TargetExt)
对Target Name,可以不是wfreerdp-server-last,只要改为一个不存的*.lib的就行。
由于wfreerdp-server.lib改为了wfreerdp-server-last.lib,用到该库的wfreerdp-server-cli须要改它的“Linker/Input/Additional Dependencies”,当中的wfreerdp-server.lib改为wfreerdp-server-last.lib
二、顶层逻辑
可把整个server归为四个步骤。一是解析命令行参数,二是侦听3389端口、启动3389侦听线程,三是不断运行、直到侦听线程结束,四是退出app。
2.1 解析命令行参数
相比于client,server命令行参数要简单很多。实用的可能就是修改3389端口
wfreerdp-server.exe 3390
用以上参数后可把端口改到3390。即使是为学习FreeRDP代码,也不建议修改这个端口。因为调试时,client往往会用mstsc.exe,mstsc默认只会连3389的server
这阶段会创建抓屏线程:wf_update_thread,句柄赋给updateThread成员变量。但不会运行。运行要在第三步骤,要有一个client完成了连接阶段。
2.2 侦听3389端口、启动侦听线程
BOOL wfreerdp_server_start(wfServer* server);
- instance.Open(Open是函数指针,指向freerdp_listener_open)。在本地侦听3389端口。设备有N个“网卡”地址,它就会调用N次socket、bind、listen。所有3389的侦听socket运行在非阻塞模式。
- 创建并运行3389侦听线程:wf_server_main_loop,句柄赋给thread成员变量。它的运行过程基本就是server的整个生命周期。
2.3 不断等待,直到侦听线程wf_server_main_loop退出
wf_server_main_loop负责侦听是否有client连向了3389,不是处理具体事务的线程,举个例子,它不处理任何的收发RDP PDU。当侦听到有client要求建立新连接后,会调用freerdp_listener_check_fds。后者首先accept得到client ip,然后调用wf_peer_accepted创建针对该client的会话处理线程wf_peer_main_loop。wf_peer_main_loop才是具体事务线程,像收发RDP PDU,为什么要使用1+N线程模式?因为同一时刻可能有N个client连接着3389。
wf_server_main_loop主要工作是处理新连接,因而绝大多数时间是花在select。至于它何时退出?——好像最多用的是手动退出app。
2.4 退出app
BOOL wfreerdp_server_stop(wfServer* server); void wfreerdp_server_free(wfServer* server);
wfreerdp_server_stop结束3389服务,void wfreerdp_server_free释放相关资源。
三、抓屏线程:wf_update_thread
抓屏线程由主线程创建,创建时机是在main解析参数前。但那时只是创建,运行要有一个client完成了连接阶段,即会话处理线程wf_peer_main_loop收到了client发来的DATA_PDU_TYPE_FONT_LIST pud、并成功处理。
对Win10,抓屏使用的技术是DXGI。
- wf_info_update_changes(wfi)。获取此时的屏幕数据放在gAcquiredDesktopImage(类型:ID3D11Texture2D*),数据类型是位置在GPU的2d纹理。
- wf_info_have_updates。简单的“wfi->framesWaiting == 0”判断,没意外总是返回true。
- wf_update_encode。可分为三步。1)wf_info_find_invalid_region。汇集此次“脏”短形为一个大矩形wfi->invalid。2)wf_info_getScreenData根据wfi->invalid“扣”出gAcquiredDesktopImage中的图像数据到pDataBits指示的内存块。3)rfx_compose_message。形成可供rdp发送server到client的rfx pdu。
- SetEvent(((wfPeerContext*)wfi->peers[index]->context)->updateEvent)。唤醒各会话线程wf_peer_main_loop,告知有新的屏幕图像pdu了。wf_peer_main_loop被唤配后,会调用wf_update_peer_send把pdu发送出去。
- wf_info_clear_invalid_region。清除和此次时间片处理相关的变量。SetRectEmpty(&wfi->invalid)。
四、CredSSP、用户登陆
4.1 CredSSP
RDP要用CredSSP把username和password从client发送到server。见图2,client在发第三个TSRequest时,必填的authInfo包含着username和password,双方前面两个TSRequest,目的就是为了安全、正确收发第三个。
用过win10自带的mstsc.exe,可能会遇到这个错误:这可能是由于CredSSP加密Oracle修正。遇到这个错误要怎么办,可通过修改client端的“加密Oracle修正(高版本叫加密数据库修正)”改为最低安全级别。
“加密Oracle修正(高版本叫加密数据库修正)”改为最低安全级加的“易受攻击”。client向server发CredSSP请求时将使用安全级别更高的加密方法,但server是老版本,不支持这种级别更高的加密码方法,于是报这个错误。针对freerdp-server,它不支持这种加密方法,于是也出些同样问题。要解决这问题的方法和mstsc.exe一样,在运行client的win10,“加密Oracle修正”改为最低安全级加的“易受攻击”。
深入代码,这错误是何时发生的?——在server收到client发来第二个TSRequest。第二个TSRequest目的是双方协商pubKeyAuth,具体步骤是client生成pubKeyAuth,发送前先sha256签名、加密,server收到后解密、验证签名,但server解密时调用DecryptMessage,解密失败了。
SECURITY_STATUS nla_decrypt_public_key_hash(rdpNla* nla) { ...... status = nla->table->DecryptMessage(&nla->context, &Message, nla->recvSeqNum++, &pfQOP); // 执行DecryptMessage,status指示失败,错误码:SEC_E_UNSUPPORTED_FUNCTION。 }

注1:client如何计算pubKeyAuth。
发送了第一个TSRequest后,state进入NLA_STATE_NEGO_TOKEN状态。收到server发来的针对第一个TSRequest应答后,freerdp调用nla_encrypt_public_key_hash,它将生成pubKeyAuth。
CredSSP Client-To-Server Binding Hash\0 (38) <---值是固定字符串,38字节 +ClientNonce (32) <---都是32字节 +PublicKey (270) <----都是270字节,值来自于tls阶段 (340)==>hash256, 32个字节
注2:authInfo内容是TSCredentials结构。credType固定是1,即credentials包含的是TSPasswordCreds结构。TSPasswordCreds内,domain:空;user:用户名;password:密码。
4.2 用户登陆机制

步骤1。client向server发出请求,用户:macbookpro15.2,但server上正有其它用户在用,于是client弹出图3。如果client一直不选择“是”,并且server没有收到client发来的pdu,像鼠标移动导致的Fast-Path Input Event,在经过N秒(N=30)后,server就向client发出Server Set Error Info PDU,4字节的errorInfo设为ERRINFO_LOGOFF_BY_USER(0xc)。

步骤2。图3中client选择了“是”,server收到后,发现另外一个用户正在使用桌面,于是弹出图4要求server操作人选择接下怎么操作。这里有三种情况,一是一直不操作,即不选择“确定”、也不选择“取消”,N秒后(N=30),server会视为“确定”,进入步骤3。二是选择“确定”,进入步骤3。三是选择“取消”,server向client发Server Save Session Info PDU,4字节的infoType设为INFOTYPE_LOGON_EXTENDED_INFO(0x3)。ErrorNotificationType置为LOGON_MSG_DISCONNECT_REFUSED(0xFFFFFFF9),同时ErrorNotificationData值是LOGON_WARNING(0x00000003)。
步骤3。server切换到client登陆的用户macbookpro15.2,这时会连续发出下面三个pdu。
Server Set Error Info PDU。4字节的errorInfo设为ERRINFO_NONE(0) Server Fast-Path Update PDU。updateCode:5, fragmentation:0, compression:2 Save Session Info Data PDU。UserName就是登陆的macbookpro15.2。
4.3 去掉CredSSP
为什么要去掉CredSSP?设想这么个应用,要在Android上建RDP Server,client使用win10内置的mstsc.exe。
- Android server没必要实现Win10的多用户切换机制。CredSSP作用是把username、password由client传到server,功能上是可以不要CredSSP。
- 一旦使用CredSSP,mstsc.exe会使用“较高等级”加密算法(似类“加密Oracle修正”),Android上的ssl库可能无法解密。
- 去除CredSSP后,只是不再传username、password,对后绪数据传输还是基于TLS,没并降低高全性。
由此可看出,一些场合有必要去掉CredSSP,以下是去掉CredSSP后的连接阶段。用的client是Windows 10 Version 1903自带的mstsc.exe。
- CR(Connection Request)中的requestedProtocols指示client能支持安全协议,值0xb,即同时支持PROTOCOL_HYBRID_EX、PROTOCOL_HYBRID和PROTOCOL_SSL。
- 收到client发来的requestedProtocols后,server检查自已的安全能力,如果不修改源码,server安全能力是PROTOCOL_HYBRID(因为NlaSecurity=TRUE)、PROTOCOL_SSL(因为TlsSecurity=TRUE),结合client发来、协商出的值是PROTOCOL_HYBRID(2),并通过CC中的requestedProtocols发送出去。
- client发现此次会话要使用PROTOCOL_HYBRID,发出CredSSP阶段的第一个TSRequest。
- server收到第一个TSRequest后,调用nla->table->AcceptSecurityContext(函数指针,指向winpr_AcceptSecurityContext)。sspi_GetSecurityFunctionTableAByNameA由Name得到table,返回的table是nullptr,出错,返回SEC_E_SECPKG_NOT_FOUND,向client发指示失败TSRequest(还是没有errorcode,而是pubKeyAuth字段是0字节)。——定义了WITH_NATIVE_SSPI宏时,AcceptSecurityContext将指向win32 api(PSecurityFunctionTable中的AcceptSecurityContext成员变量),server也不会发失败。
- client收到server发来的应答,发现本该有效的pubKeyAuth字段是0字节,认为server不支持CredSSP。断开3389连接。重新发CR,requestedProtocols值由0xb变成了0x1。
- 收到client发来的requestedProtocols后,server检查自已的安全能力,结合client发来,协商出的值是PROTOCOL_SSL(1),并通过CC中的requestedProtocols发送出去。
- client发现此次会话要使用PROTOCOL_SSL,于是略过CredSSP,向server发Client MCS Connect Initial PDU with GCC Conference Create Request。
如果明确知道server不支持PROTOCOL_HYBRID,可以把NlaSecurity初始值设为FALSE,这样在第2步CC中的requestedProtocols值就是PROTOCOL_SSL,直接进入第7步。
为模拟去除CredSSP,在windows时可修改config.h,不定义WITH_NATIVE_SSPI
<FreeRDP>/out/config.h /* #undef WITH_CUPS */ // #define WITH_NATIVE_SSPI // 在windows,它默认是定义的 /* #undef WITH_JPEG */