This paper mainly focuses on how to provide HttpDNS service for all network requests of Android applications, and analyzes how to realize pluggable access mode through hook. It also introduces the evolution of technical solutions from Native layer to Java layer and summarizes the problems and solutions encountered.
Due to some ISP’s LocalDNS problems, users often get a suboptimal DNS resolution result, resulting in slow network access. There are no more than three reasons: first, ISP’s LocalDNS cache; Second, ISP forwards DNS requests to other ISPs to save costs. Third: When ISP recursively resolves DNS, NAT resolution errors may occur, resulting in incorrect egress IP addresses. These problems have encouraged Internet companies to launch their own DNS services, also known as HttpDNS. The traditional DNS protocol is implemented through UDP, and HttpDNS uses Http to access their own DNS servers. HttpDNS is a global traffic scheduling service. It can be used as a global traffic scheduling service.
For Android applications, how do we access HttpDNS? First, you need to find a available HttpDNS server, such as Tencent cloud HttpDNS server or Ali cloud HttpDNS server, these services are to let the client submit a domain name, and then return several IP resolution results to the client, after getting the IP, If the client simply replaces the domain name of a local network request with an IP, there are many problems:
- 1. How to verify domain names using Https
- 2. How to deal with THE SNI problem? A server uses multiple domain names and certificates, and the server does not know which certificate to provide.
- 3. How to host resource requests in WebView
- 4. How do we provide HttpDNS for network requests from third-party components
- …
More than four, tencent and ali cloud access document corresponding solutions are provided for the former three are, however, is not only the fourth problem cannot be solved, tencent and ali cloud solution for other points also is not perfect, because they have a common problem, cannot handle all in one place and unified network DNS, These issues need to be addressed on a case-by-case basis where network requests are used, and this way of accessing HttpDNS is too intrusive to be pluggable.
Are there other, less invasive ways? Let’s explore several Hook ways to provide global HttpDNS services for Android applications.
Native hook
With the help of the CONNECT method in the NDK, the HOOK implementation handles domain name resolution (see Android hacking: Hooking system functions used by Dalvik), and we have been using HttpDNS in this way for a long time. However, since Android 7.0, the system will prevent applications from dynamically linking to non-public NDK libraries. This library may cause your application to crash. See Android 7.0 behavior Changes.
The behavior that the application is expected to display based on the proprietary native library it uses and the level of the target API (Android :targetSdkVersion)
To sum up, starting from Android 7.0, if the Target API is less than or equal to 23, the first time the app is launched, the Toast prompt will be displayed. If the Target API is greater than or equal to 24, the Toast prompt will be displayed. Then Crash.
Although our Target API is only 23 at present, only Toast will pop up on some mobile phones, but sooner or later we will face the problem of Crash mentioned above, so we began to explore new ways to Hook. The Native layer cannot work, so we have to find a new way in the Java layer.
Java hook
QQ mailbox Android side of the network request is mainly divided into two kinds, one kind of Http traffic, such as their own cgi requests are Http traffic, the other kind of direct Socket, which is mainly request external domain mailbox (163,126, etc.), and our HttpDNS service, only provides the resolution of Tencent domain name, Resolution of external domain names is not supported, so we can actually provide HttpDNS resolution only for the Http traffic portion.
Let’s take a look at how Http requests at the Java layer are currently made in two ways.
- Use HttpURLConnection directly, or third-party libraries such as Android-Async-HTTP or Volley based on HttpURLConnection encapsulation. Note that only HttpURLConnection is mentioned here; HttpsURLConnection is included by default for ease of writing
- Use OkHttp. Http1.x, Http2.0, SPDY. Http is implemented with a slash-and-burn approach from Socket to Socket. Yes, it is, but this question will be explained later.)
So, we can then provide HttpDNS services for these two scenarios, starting with OkHttp,
OkHttp
OkHttp opens the DNS interface as shown in the following code. We can set up a custom DNS service for each OkHttpClient. If none is set, OkHttpClient will use a default DNS service.
We can set up our HttpDNS service for each OkHttpClient, but this approach is not a permanent one. We need to manually modify each additional OkHttpClient, and there is nothing we can do about the OkHttpClient in the third party library. Another way to think about it is to replace the default Dns implementation, dns. SYSTEM, with reflection, once and for all.
Here is the code for the Dns interface
/** * A domain name service that resolves IP addresses for host names. Most applications will use the * {@linkplain #SYSTEM system DNS service}, which is the default. Some applications may provide * their own implementation to use a different DNS server, to prefer IPv6 addresses, to prefer IPv4 * addresses, or to force a specific known IP address. * * <p>Implementations of this interface must be safe for concurrent use. */ public interface Dns { /** * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance. */ Dns SYSTEM = new Dns() { @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { if (hostname == null) throw new UnknownHostException("hostname == null"); return Arrays.asList(InetAddress.getAllByName(hostname)); }}; /** * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp. If * a connection to an address fails, OkHttp will retry the connection with the next address until * either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded. */ List<InetAddress> lookup(String hostname) throws UnknownHostException; }Copy the code
HttpURLConnection
HttpURLConnection, in addition to itself, also includes all third-party network libraries based on HttpURLConnection encapsulation, such as Android-Async-HTTP, Volley and so on. So, how do we uniformly handle all HttpURLConnection DNS?
Let’s start with the question that I mentioned earlier,
Since Android 4.4, the implementation of HttpURLConnection uses the OkHttp implementation.
So how do HttpURLConnection and OkHttp come together? Before I read the OkHttp code, I thought OkHttp was just like any other web library. It was also based on HttpURLConnection encapsulation, extended caching mechanism, concurrency management, etc. HttpURLConnection for Android is also implemented based on OkHttp. Is it not a chicken and egg problem? What came first was the chicken or the egg? This question now seems naive, but the final answer is that the OkHttp implementation is not based on HttpURLConnection, but is reimplemented from Socket itself.
Back to the question, how does HttpURLConnection switch from the kernel implementation to the OkHttp implementation? Let’s look at the code to find the answer. We usually build HttpURLConnection like this
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();Copy the code
Next, look in the URL class to see how HttpURLConnection is built,
/**
* The URLStreamHandler for this URL.
*/
transient URLStreamHandler handler;
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}Copy the code
Continue looking for an implementation of this URLStreamHandler
static URLStreamHandlerFactory factory; public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) { synchronized (streamHandlerLock) { if (factory ! = null) { throw new Error("factory already defined"); } SecurityManager security = System.getSecurityManager(); if (security ! = null) { security.checkSetFactory(); } handlers.clear(); factory = fac; } } /** * Returns the Stream Handler. * @param protocol the protocol to use */ static URLStreamHandler getURLStreamHandler(String protocol) { URLStreamHandler handler = (URLStreamHandler)handlers.get(protocol); if (handler == null) { boolean checkedWithFactory = false; // Use the factory (if any) if (factory ! = null) { handler = factory.createURLStreamHandler(protocol); checkedWithFactory = true; } / /... // Fallback to built-in stream handler. // Makes okhttp the default http/https handler if (handler == null) { try { if (protocol.equals("file")) { handler = (URLStreamHandler)Class. forName("sun.net.www.protocol.file.Handler").newInstance(); } else if (protocol.equals("ftp")) { handler = (URLStreamHandler)Class. forName("sun.net.www.protocol.ftp.Handler").newInstance(); } else if (protocol.equals("jar")) { handler = (URLStreamHandler)Class. forName("sun.net.www.protocol.jar.Handler").newInstance(); } else if (protocol.equals("http")) { handler = (URLStreamHandler)Class. forName("com.android.okhttp.HttpHandler").newInstance(); } else if (protocol.equals("https")) { handler = (URLStreamHandler)Class. forName("com.android.okhttp.HttpsHandler").newInstance(); } } catch (Exception e) { throw new AssertionError(e); }} / /... } return handler; }Copy the code
Here, we found the shadow OkHttp, Android here reflects access com. Android. OkHttp. An HttpHandler and com. Android. OkHttp. HttpsHandler, can go to the AOSP external module found in them, They’re all implementations of URLStreamHandler,
The responsibility of URLStreamHandler is primarily to build URLConnection. One other thing we can notice in the getURLStreamHandler code above is that there is a factory implementation of URLStreamHandler, URLStreamHandlerFactory Factory, which is null by default, If we give it an implementation, we can let the system get our custom URLStreamHandler through this factory, which is the key to handling all httpurLConnections uniformly. We simply provide our system with a custom URLStreamHandlerFactory that returns a custom URLStreamHandler that returns the URLConnection for which we provided the HttpDNS service.
Now that we have a rough idea of how to handle all HttpurlConnections uniformly, there are two questions to consider:
- 1, how to implement a custom
URLStreamHandlerFactory
- 2. Which version of OkHttp does Android use?
For details on how to implement a custom URLStreamHandlerFactory, see the OkHttp module called okHTTP-URlConnection. This module builds an OKHTTP-based URLStreamHandlerFactory.
In a custom factory, we can set up a custom DNS service for OkhttpClient, so we can set up a custom DNS service for OkhttpClient as before, and we will implement the global HttpDNS service for HttpURLConenction.
Also, the core code for the okHTTP-urlConnection module is marked deprecated.
/**
* @deprecated OkHttp will be dropping its ability to be used with {@link HttpURLConnection} in an
* upcoming release. Applications that need this should either downgrade to the system's built-in
* {@link HttpURLConnection} or upgrade to OkHttp's Request/Response API.
*/
public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable {
//...
}Copy the code
Rest assured, we found, on the external AOSP/okhttp mentioned com. Android. Okhttp. An HttpHandler as well as the implementation of the principle, so it seems, still can continue to use this way. Deprecated as mentioned above, not because the interface is unstable, but because OkHttp officials want amway to use the standard OkHttp API.
Another question, which version of OkHttp will the Android system use? The following is the latest version of OkHttp on the AOSP Master branch as of now
Which version of OkHttp is used by AOSP
The Android Framework uses only OkHttp2.6. For some reason, the OkHttp version has not been updated. Take a look at OkHttp changelog. md, which has been updated from version 2.6 to the latest stable version 3.8.1. A number of bugfixes and features have been added to improve stability. So, if we provide a custom URLStreamHandlerFactory for our application, we have the added benefit of enabling HttpURLConnection to get the latest Okhttp optimizations.
In addition, there are many other things you can do, such as using Interceptors based on the responsibility chain mechanism to do Http traffic capture tool, or Http traffic monitoring tool, see Chuck.
So far, we have been able to handle all Http traffic by adding an HttpDNS service to it. Although this has satisfied our business, it is not enough. As a general solution, we need to provide an HttpDNS service for TCP traffic, that is, how to handle all Socket DNS. If a unified HttpDNS service is provided for sockets, there is no need to handle DNS for Http traffic.
How to handle DNS of all sockets globally
We have considered two approaches to this problem. The first approach is to use SocketImplFactory to build a custom SocketImpl. This approach is a bit more complex than the second approach, which has not been implemented yet, but this approach has another powerful advantage, which is to achieve global traffic monitoring. Traffic monitoring might then be done around it. Here’s another way to do it.
We started with the default DNS resolution process of Android applications, and found that the default DNS resolution is to call the following getAllByName interface
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();
}
//,,,
}Copy the code
Inet6AddressImpl is a standard interface class. We can dynamically proxy it to add our HttpDNS implementation and set the new Inet6AddressImpl reflection to the InetAddressImpl impl. Perfect problem solving.
At present, the latest version of QQ mailbox uses the custom URLStreamHandlerFactory method, and next we are going to migrate to the dynamic proxy InetAddressImpl method. However, the custom URLStreamHandlerFactory will be retained for introducing the latest OkHttp features, as well as traffic monitoring.
Problems encountered
Just a little bit about the potholes
1. The X509TrustManager fails to be obtained
If only SSLSocketFactory is set, OkHttp will attempt to reflect an X509TrustManager. Sun. Security. SSL. SSLContextImpl on Android doesn’t exist, so eventually throw Unable to extract the trust manager Crash.
public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) { if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null"); X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory); if (trustManager == null) { throw new IllegalStateException("Unable to extract the trust manager on " + Platform.get() + ", sslSocketFactory is " + sslSocketFactory.getClass()); } this.sslSocketFactory = sslSocketFactory; this.certificateChainCleaner = CertificateChainCleaner.get(trustManager); return this; } // Platform.get(). TrustManager method public X509TrustManager trustManager(SSLSocketFactory SSLSocketFactory) {// Attempt to get the trust manager from an OpenJDK socket factory. We attempt this on all // platforms in order to support Robolectric, which mixes classes from both Android and the // Oracle JDK. Note that we don't support HTTP/2 or other nice features on Robolectric. try { Class<? > sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl"); Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context"); if (context == null) return null; return readFieldOrNull(context, X509TrustManager.class, "trustManager"); } catch (ClassNotFoundException e) { return null; }}Copy the code
To solve this problem, you should rewrite the OkHttpsURLConnection class in okHTTP-URlConnection to make the following changes
@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { // This fails in JDK 9 because OkHttp is unable to extract the trust manager. delegate.client = delegate.client.newBuilder() .sslSocketFactory(sslSocketFactory) SslSocketFactory (sslSocketFactory, yourTrustManager).build(); }Copy the code
2. Proxy authentication
The authentication information of OkHttp to Proxy is obtained through a customized Authenticator interface rather than from the header. Therefore, you need to add an Authenticator to OkHttpClient for Proxy authentication when setting the authentication information of Proxy.
3. Endless loops
If your HttpDNS query interface is directly connected to the IP address, then there is no problem, can skip, if the domain name is accessed through the HttpDNS, then you need to pay attention to the domain name, otherwise it will fall into an infinite loop.
conclusion
This paper mainly focuses on how to provide HttpDNS service for all network requests of Android applications, and analyzes how to achieve pluggable access through hook. It also introduces the evolution of technical solutions from Native layer to Java layer and summarizes the problems and solutions encountered.
If there are inaccurate expressions, please also point out.