概述
移动解析 HTTPDNS 作为移动互联网时代 DNS 优化的一个通用解决方案,主要解决了以下几类问题:
LocalDNS 劫持/故障
LocalDNS 调度不准确
移动解析 HTTPDNS 的 Android SDK,主要提供了基于移动解析 HTTPDNS 服务的域名解析和缓存管理能力:
SDK 在进行域名解析时,优先通过移动解析 HTTPDNS 服务得到域名解析结果,极端情况下如果移动解析 HTTPDNS 服务不可用,则使用 LocalDNS 解析结果。
移动解析 HTTPDNS 服务返回的域名解析结果会携带相关的 TTL 信息,SDK 会使用该信息进行移动解析 HTTPDNS 解析结果的缓存管理。
前期准备
2.
服务开通后,您需在移动解析 HTTPDNS 控制台添加解析域名后才可正常使用,详情请参见
添加域名
。
SDK 接入
接入 HTTPDNS SDK
直接引入aar包
2.
aar 包引入,将
HttpDNSLibs\\HTTPDNS_ANDROID_xxxx.aar
拷贝至应用 libs 相应位置。
3.
在 App module的
build.gradle
文件中,添加如下配置:
说明:
V4.3.0至V4.8.1版本增加了本地数据存储库 room,请参考下方代码引入相关依赖。V4.9.0及后续版本无需再处理。
android {
repositories {
flatDir {
dirs 'libs'
}
}
}
dependencies {
implementation(name: 'HTTPDNS_Android_xxxx', ext: 'aar')
implementation "androidx.room:room-rxjava2:2.2.0"
}
maven资源库下载
1.
直接导入pom文件依赖
<dependency>
<groupId>io.github.dnspod</groupId>
<artifactId>httpdns-sdk</artifactId>
<version>4.4.0</version>
<type>aar</type>
</dependency>
<dependency>
<groupId>io.github.dnspod</groupId>
<artifactId>httpdns-sdk</artifactId>
<version>4.4.0-intl</version>
<type>aar</type>
</dependency>
注意:
HTTPDNS SDK V4.4.0支持国际站独立SDK,使用时初始化配置信息需在HTTPDNS国际站控制台中获取。详情请参见
国际站接入文档
。
权限配置
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
网络安全配置兼容
App targetSdkVersion ≥ 28(Android 9.0)的情况下,系统默认不允许 HTTP 网络请求,详情请参见
Opt out of cleartext traffic
。
这种情况下,业务侧需要将 HTTPDNS 请求使用的 IP 配置到域名白名单中。配置如下:
AndroidManifest 文件中配置。
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>
XML 目录下添加 network_security_config.xml 配置文件。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">119.29.29.98</domain>
<domain includeSubdomains="false">119.28.28.98</domain>
</domain-config>
</network-security-config>
SDK 初始化
初始化配置服务(4.0.0版本开始支持)
注意:
HTTPDNS SDK 提供多重解析优化策略,建议根据实际情况选配,也可以组合使用,可使得解析成功率达到最优效果。
可以通过配置
setUseExpiredIpEnable(true)
和
setCachedIpEnable(true)
来实现乐观 DNS 缓存。
该功能旨在提升缓存命中率和首屏加载速度。持久化缓存会将上一次解析结果保持在本地,在 App 启动时,会优先读取到本地缓存解析结果。
存在使用缓存 IP 时为过期 IP(TTL 过期),该功能启用了允许使用过期 IP,乐观的推定 TTL 过期,大多数情况下该 IP 仍能正常使用。优先返回缓存的过期结果,同时异步发起解析服务,更新缓存。
乐观 DNS 缓存在首次解析域名(无缓存)时,无法命中缓存,返回0;0,同时也会异步发起解析服务,更新缓存。在启用该功能后需自行 LocalDNS 兜底。核心域名建议配置预解析服务
preLookupDomains(String... domainList)
。
在获取服务实例之前,可通过初始化配置,设置服务的一些属性在 SDK 初始化时进行配置项传入。
说明:
【V4.4.0 废弃】 sdk日志上报能力由控制台控制,请
查看具体指引
。
DnsConfig dnsConfigBuilder = DnsConfig.Builder()
.dnsId("xxx")
.dnsKey("xxx")
.dnsIp("xxx")
.desHttp()
.token("xxx")
.logLevel(Log.VERBOSE)
.preLookupDomains("baidu.com", "qq.com")
.persistentCacheDomains("baidu.com", "qq.com")
.ipRankItems(ipRankItemList)
.setCustomNetStack(3)
.setUseExpiredIpEnable(true)
.setCachedIpEnable(true)
.timeoutMills(2000)
.routeIp("XXX")
.enableReport(true)
.build();
MSDKDnsResolver.getInstance().init(this, dnsConfigBuilder);
旧版本初始化(建议尽快升级)
HTTP 协议服务地址为
119.29.29.98
,HTTPS 协议服务地址为
119.29.29.99
(仅当采用自选加密方式并且
channel
为
Https
时使用
99
的 IP)。
新版本 API 更新为使用
119.29.29.99/98
接入,同时原移动解析 HTTPDNS 服务地址
119.29.29.29
仅供开发调试使用,无 SLA 保障,不建议用于正式业务,请您尽快将正式业务迁移至
119.29.29.99/98
。
使用 SDK 方式接入 HTTPDNS,若 HTTPDNS 未查询到解析结果,则通过 LocalDNS 进行域名解析,返回 LocalDNS 的解析结果。
SDK 接入业务方式
将 HTTPDNS SDK 的域名解析能力接入到业务的 HTTP(HTTPS)网络访问流程中,总的来说可以分为以下两种方式:
方式1:替换 URL
替换 URL 中的 Host 部分得到新的 URL,并使用新的 URL 进行网络访问。
这种实现方案下,URL 丢掉了域名的信息,对于需要使用到域名信息的网络请求,需进行较多的兼容性工作。
方式2:替换 DNS
将 HTTPDNS 的域名解析能力注入到网络访问流程中,替换掉原本网络访问流程中的 LocalDNS 来实现。
这种实现方案下,不需要逐个对请求的 URL 进行修改,同时由于没有修改 URL,无需进行额外的兼容性工作,但需要业务侧使用的网络库支持 DNS 实现替换。
DNS 替换也可以通过 Hook 系统域名解析函数的方式来实现,但 HTTPDNS SDK 内部已经使用系统的域名解析函数,Hook 系统域名解析函数可能会造成递归调用直到栈溢出。
不同网络库具体的接入方式,可以参见对应的接入文档(当前目录下)及参考使用 Sample(HttpDnsSample 目录)。
替换 URL 接入方式兼容
如前文所述,对于需要使用到域名信息的网络请求(一般是多个域名映射到同一个 IP 的情况),需要进行额外兼容。以下从协议层面阐述具体的兼容方式,具体的实现方式需要视网络库的实现而定。
HTTP 兼容
对于 HTTP 请求,需要通过指定报文头中的 Host 字段来告知服务器域名信息。Host 字段详细介绍参见
Host
。
HTTPS 兼容
HTTPS 是基于 TLS 协议之上的 HTTP 协议的统称。对于 HTTPS 请求,同样需要设置 Host 字段。
在 HTTPS 请求中,需要先进行 TLS 的握手。TLS 握手过程中,服务器会将自己的数字证书发给我们用于身份认证,因此,在 TLS 握手过程中,也需要告知服务器相关的域名信息。在 TLS 协议中,通过 SNI 扩展来指明域名信息。SNI 扩展的详细介绍参见
Server Name Indication
。
本地使用 HTTP 代理
说明:
本地使用 HTTP 代理情况下,建议
不要使用 HTTPDNS
进行域名解析。
以下区分两种接入方式并进行分析:
替换 URL 接入
根据 HTTP/1.1 协议规定,在使用 HTTP 代理情况下,客户端侧将在请求行中带上完整的服务器地址信息。详细介绍可以参见
origin-form
。
这种情况下(本地使用了 HTTP 代理,业务侧使用替换 URL 方式接入了 HTTPDNS SDK,且已经正确设置了 Host 字段),HTTP 代理接收到的 HTTP 请求中会包含服务器的 IP 信息(请求行中)以及域名信息(Host 字段中),但具体 HTTP 代理会如何向真正的目标服务器发起 HTTP 请求,则取决于 HTTP 代理的实现,可能会直接丢掉我们设置的 Host 字段使得网络请求失败。
替换 DNS 实现
以 OkHttp 网络库为例,在本地启用 HTTP 代理情况下,OkHttp 网络库不会对一个 HTTP 请求 URL 中的 Host 字段进行域名解析,而只会对设置的 HTTP 代理的 Host 进行域名解析。在这种情况下,启用 HTTPDNS 没有意义。
您可通过以下代码,
判断本地是否使用 HTTP 代理
:
val host = System.getProperty("http.proxyHost")
val port = System.getProperty("http.proxyPort")
if (null != host && null != port) {
}
接入验证
日志验证
当 init 接口中设置.logLevel(Log.VERBOSE),过滤 TAG 为 “HTTPDNS” 的日志,并查看到 LocalDns(日志上为 ldns_ip)和 HTTPDNS(日志上为 hdns_ip)相关日志时,可以确认接入无误。
key 为 ldns_ip 的是 LocalDNS 的解析结果。
key 为 hdns_ip 的是 HTTPDNS A 记录的解析结果。
key 为 hdns_4a_ips 的是 HTTPDNS AAAA 记录的解析结果。
key 为 a_ips 的是域名解析接口返回的 IPv4 集合。
key 为 4a_ips 的是域名解析接口返回的 IPv6 集合。
模拟 LocalDNS 劫持
模拟 LocalDNS 劫持情况下,若 App 能够正常工作,可以证明 HTTPDNS 已经成功接入。
注意:
由于 LocalDNS 存在缓存机制,模拟 LocalDNS 进行接入验证时,请尽量保证 LocalDNS 的缓存已经被清理。您可以通过重启机器,切换网络等方式,尽量清除 LocalDNS 的解析缓存。验证时,请注意对照启用 LocalDNS 和启用 HTTPDNS 的效果。
修改机器 Hosts 文件。
LocalDNS 优先通过读取机器 Hosts 文件方式获取解析结果。
通过修改 Hosts 文件,将对应域名指向错误的 IP,可以模拟 LocalDNS 劫持。
Root 机器可以直接修改机器 Hosts 文件。
修改 DNS 服务器配置。
通过修改 DNS 服务器配置,将 DNS 服务器指向一个不可用的 IP(如局域网内的另一个 IP),可以模拟 LocalDNS 劫持。
机器连接 Wi-Fi 情况下,在当前连接的 Wi-Fi 的高级设置选项中修改 IP 设置为静态设置,可以修改 DNS 服务器设置(不同机器具体的操作路径可能略有不同)。
借助修改 DNS 的 App 来修改 DNS 服务器配置(通常是通过 VPN 篡改 DNS 包的方式来修改 DNS 服务器配置)。
抓包验证
以下以接入 HTTP 网络访问为例进行说明:
注意:
常用的移动端 HTTP/HTTPS 抓包工具(例如 Charles/Fiddler),是通过 HTTP 代理方式进行抓包,不适用于抓包验证 HTTPDNS 服务是否生效,相关说明请参见
本地使用 HTTP 代理
。
使用
tcpdump
进行抓包。
Root 机器可以通过 tcpdump 命令抓包。
非 Root 机器上,系统可能内置有相关的调试工具,可以获取抓包结果(不同机器具体的启用方式不同)。
通过
WireShark
观察抓包结果。
对于 HTTP 请求,我们可以观察到明文信息,通过对照日志和具体的抓包记录,可以确认最终发起请求时使用的 IP 是否和 SDK 返回的一致。如下图所示:
从抓包上看,
xw.qq.com
的请求最终发往了 IP 为
183.3.226.35
的服务器。
对于 HTTPS 请求,TLS 的握手包实际上是明文包,在设置了 SNI 扩展(请参见
HTTPS 兼容
)情况下,通过对照日志和具体的抓包记录,可以确认最终发起请求时使用的 IP 是否和 SDK 返回的一致。如下图所示:
从抓包上看,
xw.qq.com
的请求最终发往了 IP 为
183.3.226.35
的服务器。
注意事项
getAddrByName 是耗时同步接口,应当在子线程调用。
如果客户端的业务与 HOST 绑定,例如客户端的业务绑定了 HOST 的 HTTP 服务或者是 CDN 的服务,那么您将 URL 中的域名替换成 HTTPDNS 返回的 IP 之后,还需要指定下 HTTP 头的 HOST 字段。
以 URLConnection 为例:
URL oldUrl = new URL(url);
URLConnection connection = oldUrl.openConnection();
String ips = MSDKDnsResolver.getInstance().getAddrByName(oldUrl.getHost());
String[] ipArr = ips.split(";");
if (2 == ipArr.length && !"0".equals(ipArr[0])) {
String ip = ipArr[0];
String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
connection = (HttpURLConnection) new URL(newUrl).openConnection();
connection.setRequestProperty("Host", oldUrl.getHost());
}
以 curl 为例,假设您想要访问
www.qq.com
,通过 HTTPDNS 解析出来的 IP 为
192.168.0.111
,则访问方式如下:
curl -H "Host:www.qq.com" http://192.168.0.111/aaa.txt
检测本地是否使用了 HTTP 代理。如果使用了 HTTP 代理,建议
不要使用 HTTPDNS
做域名解析。示例如下:
String host = System.getProperty("http.proxyHost");
String port= System.getProperty("http.proxyPort");
if (null != host && null != port) {
}