What happens: When the app gets really big, especially in large companies, we introduce aar packages from other departments. Even though the main project of our own app uses HTTPDNS, it is inevitable that other AAR packages will also use our HTTPDNS service, because it is predictable that other departments will not use the network framework you use, although everyone is using OHHTTP now. For example, some departments still use HttpurlConnection or even use protocols other than HTTP, such as RTMP provided by multimedia departments. If these lines of business force them to use our HTTPDNS service, there will be a lot of code to change, and there will be a lot of cross-departmental communication problems. Is there a way to change the DNS service at the global level?
The answer is yes! (The following analysis is based on android-27 source code, different source versions of the following ideas are different, the use of production needs to do model adaptation.)
After reviewing okHTTP, HttpurlConnection and RTMP codes, we found that all DNS queries on the Java layer use InetAddress
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];
}
Copy the code
The obvious question is what is the IMPL? So I went in and I found
Inet6AddressImpl originally provided the service. Then follow:
AddressCache = AddressCache = 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)); }}Copy the code
AddressCache is actually a map that stores the relationship between domain names and IP addresses. So if we want to change the global DNS query result we just need to change the AddressCache content. Because they are statically final and globally unique.
The AddressCache put or get method is a reflection hook. Here’s a demo:
/** * We can hook addressCache as well. We can derive addressCache from addressCache. Note that AddressCache is a hide class, so we need to throw new RuntimeException("Stub!"); * So that we can refer to this class in our app code * * here only provides the idea of hook method. Hook object you do it yourself. */ void hookInetAddress(String host, String IP) {// Get the bytecode file object Class c; InetAddress {//addressCache is a static variable in InetAddress. The addressCache is a static variable in InetAddress. C = class.forname (); c = class.forname (); c = Class."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(); }}Copy the code
Finally, let’s test whether we succeeded in hook:
// Use 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();
Copy the code
Ok, success. We can now unify our DNS services at the Java level.