查看原文
其他

技术实践|网易云信 IM SDK 服务高可用技术方案

郝魁 网易智企技术+ 2021-09-19


导读:“域名劫持是互联网攻击的一种方式,通过攻击域名解析服务器(DNS),或伪造域名解析服务器(DNS)的方法,把目标网站域名解析到错误的 IP 地址从而实现用户无法访问目标网站的目的,或者蓄意/恶意要求用户访问指定 IP 地址(网站)的目的。”(以上内容引自「域名劫持」百度百科) 。网易云信 IM SDK 作为一款 ToB 产品,支撑着各种三方业务的开展面对各种复杂的网络环境,DNS 劫持与 DNS 污染时有发生,那么在我们提供服务的过程中应该如何避免此类事故的发生呢?

文|郝魁

网易云信资深 C++ 开发工程师



技术是一把双刃剑,在大侠手中定国安邦,在鼠辈手中祸国殃民。


“域名劫持”虽然带了“劫持”二字,但在此环境中实属中性词汇。例如对于一些非法网站的访问,可以通过 DNS 服务把相应的域名解析到不可访问的 IP 地址,以阻止对于该非法网站的访问并给予警告等。在网易云信即时通信产品运维过程中,曾经发生过服务域名“ netease.im ”被别有用心的人或组织恶意劫持的事件,导致接入网易云信 IM SDK 的应用无法正常登录,给客户以及客户的用户造成影响。


为了弄清楚这种事故是怎么发生的,我们来分析一下网易云信 IM SDK 的登录过程:



从流程上来看,“更新 LBS”的节点上,如果发生 DNS 劫持,在访问网易云信 LBS 服务时有可能超时或者拿到了错误的应答,导致 IM SDK 无法获取正常的 Link 服务器地址及端口,如何避免此类事故的发生呢?


本文将围绕网易云信端侧服务高可用技术方案以及高可用组件实现方案进行具体分享。


如何预防 DNS 劫持


通常对于域名被劫持后,我们可以采用以下几种方式:



上述几种方式,都是在发生劫持后采取的方案,无论是从服务提供侧还是服务使用侧来说都不够灵活,为了解决这些问题,提前预防事故的发生,我们主要采用以下两种方案:



其中接入了 HttpDNS 服务的方案降低了所有场景下域名被劫持的风险。


 1、LocalDNS 域名劫持 


域名劫持一直是困扰许多开发者的问题之一,其表现为域名 A 应该返回的 DNS 解析结果 IP1 被恶意替换为了 IP2,导致 A 的访问失败或访问了一个不安全的站点,常见域名劫持方式有以下几种:


  • 黑客入侵宽带路由器,篡改终端用户 LocalDNS,并指向伪造 LocalDNS,通过控制 LocalDNS 的逻辑返回错误的 IP 信息进行域名劫持。

  • 监听终端用户域名解析请求,在 LocalDNS 返回正确结果之前将伪造的 DNS 解析响应传递给终端用户,进而控制终端用户的域名访问行为。

  • 缓存污染,LocalDNS 缓存了域名的解析结果,并对部分域名结果进行更改,导致用户访问被指向被更改的地址,示意图如下:



 2、HttpDNS 实现原理


Step 1客户端直接访问 HttpDNS 接口,获取业务在域名配置管理系统上配置的“正确的”、“访问速度最优的”IP 列表。


Step 2:客户端向获取到的 IP 后就向直接往此 IP 发送业务协议请求。以 HTTP 请求为例,通过在 header 中指定 Host 字段,向 HttpDNS 返回的 IP 发送标准的 HTTP 请求即可,如果是 Https 还要考虑 SNI 的问题。



网易云信服务高可用策略


为了提高服务的高可用性,网易云信 SDK 接入了 HttpDNS 服务,高可用方案整体结构如下所示:




IM SDK 接入 HttpDNS 服务实现服务高可用的一般流程:



1、端侧 HttpDNS SDK 实现


IM SDK 高可用组件采用了跨平台开发方案,主要针对 Native SDK(Windows 、acOS 、iOS 、Android)进行了支持,基本结构如下:



高可用组件为了保证高可用性、响应的及时性、结果的正确性,在设计时需要完成以下几个功能:

  • HttpDNS 服务接口的更新及缓存维护

  • 域名查询结果更新及缓存维护

  • HTTP 请求的实现


1.1 阶梯式 HTTP 请求


某一域名通过 HttpDNS 的域名查询服务,可能会解析出多个 IP 地址,如果这些地址当中包含已下线或者访问速度不理想的节点,会使整个 HTTP 的访问时间变长,最坏的情况是所有的地址都进行了访问,且都超时,如下图所示:


int kRequestTimeout = 30*1000;//30秒list<string> lstURLs = { "https://192.168.1.1/xxx", "https://192.168.1.2/xxx", ...... , "https://192.168.1.n/xxx", "https://192.168.1.n+1/xxx"};int nMaxTimeout = lstURLs.size() * kRequestTimeout;


为了提高 HTTP 请求的访问效率,在网易云信高可用组件中引入了多地址阶梯式的 HTTP 请求机制,比如单一链接的 HTTP 请求超时时间指定为 30s,当开始第一个链接访问时开启一个超时时间为 3 秒的定时器 Timer0,如果该请求在 3 秒内返回并且经过业务模块验证为正确应答,此时整个多地址阶梯式请求结束,否则在 Timer0 触发时,启动对下一链接的 HTTP 请求,并启动一个新的超时时间为 3 秒的定时器 Time1,以此类推,直到有正确的响应结果或者所有的链接都已访问完成,流程如下所示:


int kRequestTimeout = 30*1000;//30秒int kWatiTimeout = 3*1000;//3秒list<string> lstURLs = { "https://192.168.1.1/xxx", "https://192.168.1.2/xxx", ...... , "https://192.168.1.n/xxx", "https://192.168.1.n+1/xxx"};int nMaxTimeout = (lstURLs.size() - 1) * kWatiTimeout + kRequestTimeout;


1.2 HttpDNS 服务接口的更新及缓存维护


HttpDNS 也是一个 HTTP 服务也存在被劫持的可能,所以除了使用 HttpDNS 域名外,高可用组件还内置了多个固化的 IP,以解决 HttpDNS 域名被劫持后无法访问的问题,使用固化 IP 虽然可以解决域名被劫持的问题,但不一定是最新、最优的节点,为了解决这个问题,高可用组件在指定时间内会更新 HttpDNS 服务地址,拿到最新、最优的节点。为了减少对 HttpDNS 的访问量,引入了服务地址的 TTL(一般是1小时)机制,即在服务地址有效期内使用本地缓存,不再向 HttpDNS 请求服务地址。


HttpDNS 服务初始化流程如下:



1.3 域名查询结果更新及缓存维护


高可用组件为了保证域名查询的及时响应,以及减少对 HttpDNS 的访问量,引入了“查询结果缓存”,对已经查询过的域名结果进行了本地缓存,为了提高正确性同样也加入了 TTL 机制(一般是 5 分钟),超出指定时间后,会对结果进行再次更新,同时为了保证响应的及时性,在 TTL 的基础上加入了冗余时间(一般是 TTL*0.75),所以调用高可用组件查询域名会存在三种结果:

  • 缓存未到达冗余时间,返回缓存结果。

  • 缓存达到冗余时间,但尚未过期,返回缓存结果,同时发起更新请求。

  • 缓存已过期,发起更新请求,应答成功后,更新缓存,响应上层调用,如果应答失败,继续使用缓存数据。


调用高可用组件进行域名查询的流程如下:



1.4 HTTP 访问流程设计



 2、疑似劫持事件上报 


当调用高可用组件进行 HTTP 请求时,如果因非网络原因导致的请求失败,而且触发了 HttpDNS 查询域名操作,则判定此次访问的域名可能存在已被劫持的风险,高可用组件会收集与此域名相关的信息内容上报到网易云信数据大盘,数据大盘后台根据上报信息来定位是否存在域名劫持的情况,并根据实际情况来决定是否要重新配置 HttpDNS 解析结果还是配合安全部门对相应的 App 进行封禁处理。


request_url

请求的 url

host_ip

LocalDNS 对域名的解析结果

platform

平台标识

error_code

请求失败时返回的 error code

timestamp

请求发送的时间

consumed

请求的耗时

business_token

业务标识


3、 SNI 处理 


为了让多个域名复用一个 IP,在 HTTP 服务器上引入了虚拟主机的概念。多个虚拟主机共享 IP 的 HTTPS 服务器中,在握手建立之前服务器无法知道客户端请求的具体 Host,所以无法将请求交给特定的虚拟主机,从而导致服务器无法读取虚拟主机中配置的证书信息。SNI 就是用来解决这个问题的,SNI 是 SSL 和 TLS 的一个扩展协议。SNI 要求客户端在与服务器握手时就携带需要访问的域名的 Host 信息,具体实现方法是在客户端“Client Hello”报文的请求头中,增加了 Server Name 的扩展字段,因此服务器便会知道需要用哪个虚拟主机的证书与客户端握手并建立 TLS 连接。


以下是使用 libcurl 发送 HTTP 请求的代码片段:

bool configureCURLRequest(CURL *curl,const std::string& url,unsigned int timeOut = 7000,const std::string& ip = "",unsigned short port = 443) { bool ret = false; do { if (curl != nullptr || url.empty()) break; if (curl_easy_setopt(curl, CURLOPT_URL, url.c_str()) != CURLE_OK) break; if (curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeOut) != CURLE_OK) break; if (curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L) != CURLE_OK) break; if(NE_NET::NimNetUtil::IsHttpsScheme(url)) { if (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L) != CURLE_OK) break; if (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L) != CURLE_OK) break; if (!ip.empty()) { if (curl_easy_setopt(curl, CURLOPT_DNS_USE_GLOBAL_CACHE, false) != CURLE_OK) break; std::string domain = NE_NET::NimNetUtil::GetDomainFromURL(url); std::string dns(domain); dns.append(":").append(std::to_string(port)).append(ip); struct curl_slist *dnsInfo = curl_slist_append(NULL, dns.c_str()); if (curl_easy_setopt(curl, CURLOPT_RESOLVE, dnsInfo) != CURLE_OK) break; } } ret = true; } while (false); return ret;}


参考资料


DNS污染_百度百科 (baidu.com)

域名劫持_百度百科 (baidu.com)

域名服务器缓存污染 - 维基百科,自由的百科全书 (wikipedia.org)


以上就是网易云信端侧服务高可用技术方案以及高可用组件实现方案的分享,如有兴趣,欢迎你在公众号留言与我探讨。


 作者介绍 


郝魁,网易云信资深 C++ 开发工程师,主要负责网易云信 IM SDK 的开发、维护、重构等工作,拥有多年 C++ 客户端开发经验,现致力于跨平台 C++ 开发。




: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存