遇到的情况: 当app变的特别大的时候,尤其是大公司,我们会引入很多其他部门的aar包。即使我们自身app的主工程使用了httpdns,但是 依旧无法避免其他aar包也会使用我们提供的httpdns服务,因为可以预见的是,不一定其他部门也会使用你使用的网络框架, 虽然大家现在都是用的ohhttp,但是特殊情况下比如有的部门还是使用httpurlconnection,甚至有的部门使用的协议都不是 http协议,比如多媒体部门提供的rtmp协议等。这些业务部门如果强制让他们使用我们的httpdns服务,代码要修改的地方 就比较多了,而且跨部门沟通问题也很大,那么有没有一种方法可以在全局层面修改dns服务呢?
答案是有的!(以下分析全部基于android-27源码,不同源码版本下面思路有别,运用到生产上需要做机型适配。)
我们在review了okhttp以及httpurlconnection以及rtmp等代码以后发现 只要是java层上面的dns查询服务都是 用的InetAddress这个类的这2个方法
public static InetAddress[] getAllByName(String host)
throws UnknownHostException {
return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
}
public static InetAddress getByName(String host)
throws UnknownHostException {
return impl.lookupAllHostAddr(host, NETID_UNSET)[0];
}
复制代码
显然问题的关键在于这个impl是什么?那么点进去看以后 发现
原来是Inet6AddressImpl 提供的服务。再接着跟:
我们看下这个AddressCache的源码:
package java.net;
import libcore.util.BasicLruCache;
class AddressCache {
private static final int MAX_ENTRIES = 16;
private final BasicLruCache<AddressCacheKey, AddressCacheEntry> cache
= new BasicLruCache<AddressCacheKey, AddressCacheEntry>(MAX_ENTRIES);
static class AddressCacheKey {
private final String mHostname;
private final int mNetId;
AddressCacheKey(String hostname, int netId) {
mHostname = hostname;
mNetId = netId;
}
@Override public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AddressCacheKey)) {
return false;
}
AddressCacheKey lhs = (AddressCacheKey) o;
return mHostname.equals(lhs.mHostname) && mNetId == lhs.mNetId;
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + mNetId;
result = 31 * result + mHostname.hashCode();
return result;
}
}
static class AddressCacheEntry {
final long expiryNanos;
AddressCacheEntry(Object value) {
this.value = value;
this.expiryNanos = System.nanoTime() + TTL_NANOS;
}
}
public void clear() {
cache.evictAll();
}
public Object get(String hostname, int netId) {
AddressCacheEntry entry = cache.get(new AddressCacheKey(hostname, netId));
// Do we have a valid cache entry?
if (entry != null && entry.expiryNanos >= System.nanoTime()) {
return entry.value;
}
// Either we didn't find anything, or it had expired.
// No need to remove expired entries: the caller will provide a replacement shortly.
return null;
}
public void put(String hostname, int netId, InetAddress[] addresses) {
cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
}
public void putUnknownHost(String hostname, int netId, String detailMessage) {
cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(detailMessage));
}
}
复制代码
整体来说,这个AddressCache其实就是map,里面存着域名和ip的关系。那么如果我们想要修改全局的dns查询结果 那么我们只要修改这个AddressCache的内容即可。因为他们都是静态final的,全局唯一。
利用反射 hook 这个AddressCache的put或者get方法即可。这里给出一个demo:
/**
* 其实除了hook方法,我们还可以hook addressCache这个对象,我们可以派生一个AddressCache对象,
* 让addressCache指向他即可。注意AddressCache是一个hide的类,我们需要拿出来throw new RuntimeException("Stub!");
* 这样在我们app代码里面才引用的到这个类
*
* 这里只提供hook方法的思路。hook对象的大家自己完成吧。
* @param host 域名
* @param ip 你httpdns查询到的最佳ip地址
*/
void hookInetAddress(String host, String ip) {
//获取类的字节码文件对象
Class c;
try {
//一定要注意这里 不同的android版本,hook的思路是一样的,但是细节有所差别,比如低版本
//addressCache就是直接存在InetAddress里面作为一个静态变量的。和现在还有一层Inet6AddressImpl
//代理是不同的 这里只是简单hook了 put方法,按需要你们还可以hook其他方法完成你们的需求
c = Class.forName("java.net.Inet6AddressImpl");
Field field = c.getDeclaredField("addressCache");
field.setAccessible(true);
Method putMethod = field.get(c).getClass().getDeclaredMethod("put", String.class, int.class, InetAddress[].class);
String[] ipStr = ip.split("\\.");
byte[] ipBuf = new byte[4];
for (int i = 0; i < 4; i++) {
ipBuf[i] = (byte) (Integer.parseInt(ipStr[i]) & 0xff);
}
putMethod.invoke(field.get(c), host, 0, new InetAddress[]{InetAddress.getByAddress(ipBuf)});
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
复制代码
最后实验一下 我们hook成功了没有:
//随便写一个ip吧,用来测试结果用
hookInetAddress("www.baidu.com", "110.110.110.110");
new Thread() {
@Override
public void run() {
try {
InetAddress[] inetArray = InetAddress.getAllByName("www.baidu.com");
Log.e("wuyue", "inetArray size=" + inetArray[0].getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}.start();
复制代码
Ok,成功。自此我们就可以在java层面上统一我们的dns服务啦。