Android 全局 DNS 解析拦截-爱代码爱编程
要拦截 DNS 解析,首先得找到系统哪个类去解析 Host 的,找到对应的 hook 点去实现。
一,去了解一些常用的联网类:
Volley 默认使用的是 HttpURLConnection。
而 HttpURLConnection 底层 DNS 解析用的是:
InetAddress.getAllByName();
Okhttp 是可以自定义 DNS 的:
new OkHttpClient().newBuilder().dns(new Dns() {
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
.....................
}
});
而 Okhttp 默认 DNS 是 Dns.SYSTEM,实际也是 InetAddress.getAllByName();
二,分析 InetAddress 源码,找到 Hook 点:
InetAddress.getAllByName(); 在 Android 7.0 之后有修改。
这里先分析 Android 7.0 之后的源码:
public class InetAddress implements java.io.Serializable {
static final InetAddressImpl impl = new Inet6AddressImpl();
public static InetAddress[] getAllByName(String host) throws UnknownHostException {
return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
}
}
// 发现 Inet6AddressImpl 是个 接口实现类
class Inet6AddressImpl implements InetAddressImpl {
.............
}
那么通过反射和动态代理,重新赋值 impl 变量,就可以拦截 DNS 解析了。
再来分析 Android 7.0 之前的源码:
public class InetAddress implements java.io.Serializable {
private static final AddressCache addressCache = new AddressCache();
public static InetAddress[] getAllByName(String host) throws UnknownHostException {
return getAllByNameImpl(host, NETID_UNSET).clone();
}
private static InetAddress[] getAllByNameImpl(String host, int netId) throws UnknownHostException {
if (host == null || host.isEmpty()) {
return loopbackAddresses();
}
// Is it a numeric address?
InetAddress result = parseNumericAddressNoThrow(host);
if (result != null) {
result = disallowDeprecatedFormats(host, result);
if (result == null) {
throw new UnknownHostException("Deprecated IPv4 address format: " + host);
}
return new InetAddress[] { result };
}
return lookupHostByName(host, netId).clone();
}
private static InetAddress[] lookupHostByName(String host, int netId) throws UnknownHostException {
// 这里会从 缓存中取
Object cachedResult = addressCache.get(host, netId);
if (cachedResult != null) {
if (cachedResult instanceof InetAddress[]) {
return (InetAddress[]) cachedResult;
}
}
.................
}
}
class AddressCache {
private static final int MAX_ENTRIES = 16;
// cache just 2s.
private static final long TTL_NANOS = 2 * 1000000000L;
static class AddressCacheEntry {
final Object value;
final long expiryNanos;
AddressCacheEntry(Object value) {
this.value = value;
this.expiryNanos = System.nanoTime() + TTL_NANOS;
}
}
// 这里 get 的时候,缓存有有效时间
// 而从 put 方法,AddressCacheEntry 构造方法里,知道有效时间是 TTL_NANOS(2s)
public Object get(String hostname, int netId) {
AddressCacheEntry entry = cache.get(new AddressCacheKey(hostname, netId));
if (entry != null && entry.expiryNanos >= System.nanoTime()) {
return entry.value;
}
return null;
}
public void put(String hostname, int netId, InetAddress[] addresses) {
cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
}
}
从源码我们了解到可以利用 addressCache 缓存,提前处理好 DNS解析,但是这个缓存又只有 2s 有效时间。
三,具体实现代码:
public class DnsUtils {
private final static String TAG = "DnsUtils";
private static final Pattern IPV4_PATTERN = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
public static void hook() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
hookN();
}
}
public static boolean isIpv4(String address) {
return IPV4_PATTERN.matcher(address).matches();
}
/**
* 7.0 之后 InetAddress 才有 impl
*/
private static void hookN() {
try {
if (LogUtils.isDebug()) {
LogUtils.d(TAG, "invoke 111:ipv6FirstN");
}
//获取InetAddress中的impl
Field impl = InetAddress.class.getDeclaredField("impl");
impl.setAccessible(true);
//获取accessFlags
Field modifiersField = Field.class.getDeclaredField("accessFlags");
modifiersField.setAccessible(true);
//去final
modifiersField.setInt(impl, impl.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
//获取原始InetAddressImpl对象
final Object originalImpl = impl.get(null);
//构建动态代理InetAddressImpl对象
Object dynamicImpl = Proxy.newProxyInstance(originalImpl.getClass().getClassLoader(), originalImpl.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果函数名为lookupAllHostAddr,并且参数长度为2,第一个参数是host,第二个参数是netId
Object originalResult = method.invoke(originalImpl, args);
if (method.getName().equals("lookupAllHostAddr") && args != null && args.length == 2 && originalResult != null) {
InetAddress[] originalAddresses = (InetAddress[]) originalResult;
return myDnsLogic(originalAddresses);
}
return originalResult;
}
});
//替换impl为动态代理对象
impl.set(null, dynamicImpl);
//还原final
modifiersField.setInt(impl, impl.getModifiers() & java.lang.reflect.Modifier.FINAL);
} catch (Throwable e) {
if (LogUtils.isDebug()) {
LogUtils.e(TAG, "invoke 555:" + e);
}
}
}
private static InetAddress[] myDnsLogic(InetAddress[] originalAddresses) {
// 做自己的逻辑;
return result;
}
/**
* 7.0之前,不能动态代理,只能 addressCache 缓存来弄。
* 而缓存只有 2S 有效时间
*/
private static void hookM(final String hostName) {
BackgroundExecutors.getGlobalExecutor().post(new Runnable() {
@Override
public void run() {
try {
if (LogUtils.isDebug()) {
LogUtils.d(TAG, "invoke 111:ipv6FirstM");
}
InetAddress[] originalAddresses = InetAddress.getAllByName(hostName);
InetAddress[] result = myDnsLogic(originalAddresses);
Class inetAddressClass = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
inetAddressClass = Class.forName("java.net.Inet6AddressImpl");
} else {
inetAddressClass = InetAddress.class;
}
Field field = inetAddressClass.getDeclaredField("addressCache");
field.setAccessible(true);
Object object = field.get(inetAddressClass);
LogUtils.d(TAG, "test 11:" + object);
Class cacheClass = object.getClass();
Method putMethod;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//put方法在api21及以上为put(String host, int netId, InetAddress[] address)
putMethod = cacheClass.getDeclaredMethod("put", String.class, int.class, InetAddress[].class);
putMethod.setAccessible(true);
putMethod.invoke(object, hostName, 0, result);
} else {
//put方法在api20及以下为put(String host, InetAddress[] address)
putMethod = cacheClass.getDeclaredMethod("put", String.class, InetAddress[].class);
putMethod.setAccessible(true);
putMethod.invoke(object, hostName, result);
}
InetAddress[] test = InetAddress.getAllByName(hostName);
if (LogUtils.isDebug()) {
for (InetAddress inetAddress : test) {
LogUtils.d(TAG, "test 777:" + inetAddress);
}
}
} catch (Throwable e) {
if (LogUtils.isDebug()) {
LogUtils.e(TAG, "invoke 666:" + e);
}
}
}
});
}
}
四,总结:
1,如果项目中只使用了 Okhttp,那么可以自定义 DNS 解析,很简单。
2,如果项目中用的其他库,所以可以参考上面的第三点的代码。
3,利用反射拦截的方式,对于7.0以下的手机,不方便,只因为缓存只有2秒,所以必须得在每次请求前调用一次 hookM 方法。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/miLLlulei/article/details/110090287