技术实践|网易云信 IM SDK 服务高可用技术方案
文|郝魁
网易云信资深 C++ 开发工程师
技术是一把双刃剑,在大侠手中定国安邦,在鼠辈手中祸国殃民。
为了弄清楚这种事故是怎么发生的,我们来分析一下网易云信 IM SDK 的登录过程:
本文将围绕网易云信端侧服务高可用技术方案以及高可用组件实现方案进行具体分享。
如何预防 DNS 劫持
通常对于域名被劫持后,我们可以采用以下几种方式:
上述几种方式,都是在发生劫持后采取的方案,无论是从服务提供侧还是服务使用侧来说都不够灵活,为了解决这些问题,提前预防事故的发生,我们主要采用以下两种方案:
其中接入了 HttpDNS 服务的方案降低了所有场景下域名被劫持的风险。
1、LocalDNS 域名劫持
域名劫持一直是困扰许多开发者的问题之一,其表现为域名 A 应该返回的 DNS 解析结果 IP1 被恶意替换为了 IP2,导致 A 的访问失败或访问了一个不安全的站点,常见域名劫持方式有以下几种:
黑客入侵宽带路由器,篡改终端用户 LocalDNS,并指向伪造 LocalDNS,通过控制 LocalDNS 的逻辑返回错误的 IP 信息进行域名劫持。
监听终端用户域名解析请求,在 LocalDNS 返回正确结果之前将伪造的 DNS 解析响应传递给终端用户,进而控制终端用户的域名访问行为。
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 请求
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;
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 服务初始化流程如下:
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++ 开发。