痛点在哪里
开发及测试过程中经常需要切换开发, 测试, 预发布等环境, 切换环境是通过修改 hosts 实现的.
android 设备修改 hosts 看似简单, 实际却会遇上不少麻烦, 特别是在公司的网络环境中. 你可能会遇到这种情况:
adb shell 进去修改 / etc/hosts, 发现只读无法修改, 需要 root 权限. 尝试各种方法还是 root 失败后, 转而想要通过电脑代理的方法避开 root, 但是这种方法需要把电脑也转网到 staff-wifi, 转网后一堆开发软件设置的代理也得跟着改, linux 环境变量也得跟着改, 总之一堆麻烦, 而且从 staff-wifi 转回开发网或办公网还需要审核, 想想就放弃了...
总结几种方法
1,root 手机, 修改 / etc/hosts.
缺点: 手机越来越难 root, 比如华为已经不允许申请手机解锁码.
2, 手机和电脑连到同一个 wifi(比如 staff-wifi), 手机设置代理到电脑 (fiddler 启动电脑代理服务), 修改电脑的 hosts.
缺点: 依赖于电脑, 而且因为手机无法连入开发网或办公网, 电脑可能需要转网.
3, 拦截 DNS 请求, 直接返回所需要的 A 记录.
思来想去, 如果可以修改 DNS 的回包, 不是也相当于修改 hosts 的效果嘛! 但怎么改? 会不会也有 root 权限问题?
如何实现方法 3
经历了一番痛苦后, 笔者决定写个工具来实现改 hosts 的效果, 一劳永逸, 于是一番查找:
cap
km 上搜了一下, 发现公司有个内部开源项目 -- cap http://pub.code.oa.com/project/home?projectName=CAP , 看介绍非常强大, 基于底层 hook 技术随便抓包改包, 看样子应该可以实现篡改 DNS 的需求. 但是笔者只是想实现改 hosts 的效果, cap 显得有点重, 作为备选方案吧.
VpnService
后面发现 android 提供了 vpn 服务框架, 基础类是 VpnService, 免 root, 能够全局拦截网络请求, 修改网络包.
能够拦截, 还能够修改, 嗯, 很好!
逮着 VpnService 如何修改网络请求, 一番 google, 发现有人已经实现了笔者需要的效果而且开源了, 项目 https://github.com/x-falcon/Virtual-Hosts .
好吧, 有人实现了, 而且代码写的挺不错, 就用它了. 接下来分析一下这个项目.
Virtual-Hosts 分析
简单来说分两步: 1,VpnService 拦截 DNS 请求. 2, 构造 DNS 回包并写回 VpnService.
1, 拦截 DNS 请求
使用 VpnService 拦截 DNS 请求很容易实现, 对 VpnService 简单配置, 然后 startService 就可以了. 以 IPV4 为例, 配置如下:
- //vpn 地址, 可以随意设置
- private static final String VPN_ADDRESS = "192.0.2.111";
- //DNS 地址, 填个正常的 dns 地址即可
- private static final String VPN_DNS4 = "8.8.8.8";
- ...
- Builder builder = new Builder();
- builder.addAddress(VPN_ADDRESS, 32);
- // 设置 DNS 服务器
- builder.addDnsServer(VPN_DNS4);
- // 所有到 VPN_DNS4 的请求都走这个 vpn.
- // 配合 builder.addDnsServer(VPN_DNS4), 就实现了所有 DNS 请求都走 vpn, 因此能够拦截到所有 DNS 请求.
- builder.addRoute(VPN_DNS4, 32);
2, 构造 DNS 回包并返回
接下来处理拦截到的包.
- FileChannel vpnInput = new FileInputStream(vpnFileDescriptor).getChannel();
- ByteBuffer bufferToNetwork = ByteBufferPool.acquire();
- // 取出一个拦截到的请求包
- int readBytes = vpnInput.read(bufferToNetwork);
- // 解析包并构造出 Packet
- Packet packet = new Packet(bufferToNetwork);
- if (packet.isUDP()) {
- deviceToNetworkUDPQueue.offer(packet);
- } else if (packet.isTCP()) {
- deviceToNetworkTCPQueue.offer(packet);
- }
这里的 DNS 请求走 UDP, 因此会走到 UDPOutput:
- Packet currentPacket = inputQueue.poll();
- //handle_dns_packet 里面利用开源库 dnsjava 解析 DNS 请求并构造回包, 填入需要的 A 记录
- ByteBuffer packet_buffer= DnsChange.handle_dns_packet(currentPacket);
- // 写入到队列, 用于返回给 VpnService
- this.outputQueue.offer(packet_buffer);
回包给 VpnService:
- FileChannel vpnOutput = new FileOutputStream(vpnFileDescriptor).getChannel();
- ByteBuffer bufferFromNetwork = networkToDeviceQueue.poll();
- vpnOutput.write(bufferFromNetwork);
传送门: https://github.com/dnsjava/dnsjava/tree/master/org/xbill/DNS
到这里就走通了通过拦截 DNS 请求实现修改 hosts 效果的整个过程, 以上只是简单分析, 感兴趣可以细读代码.
来源: https://www.qcloud.com/developer/article/1157943