All Products
Search
Document Center

HTTPDNS:HTTPS scenarios (including SNI) on Android: IP direct connection solution

Last Updated:Jan 22, 2025

Background information

This topic describes a general approach for implementing IP direct connections in HTTPS scenarios, including those with Server Name Indication (SNI), on Android platforms.

If you're an Android developer using OkHttp for network development, you can seamlessly implement IP direct connections as OkHttp offers a customizable DNS service interface. This method is simpler and more flexible compared to the general approach. For integration details, see HTTPDNS + OkHttp Integration Guide for Android.

HTTPS

An SSL/TLS handshake is required to initiate an HTTPS request. The handshake involves the following steps:

  1. The client sends a handshake request with a random number, supported algorithms, and other parameters.

  2. The server selects an algorithm, then sends back its public key certificate and a random number.

  3. The client verifies the server's certificate and encrypts a random number with the server's public key.

  4. The server decrypts the random number using its private key.

  5. Both parties generate a session ticket from the exchanged information, which serves as the encryption key for subsequent data transmission.

HTTPDNS comes into play during step 3, where the client must authenticate the server's certificate. This involves two critical checks:

  1. The client uses a locally stored root certificate to validate the server's certificate chain, ensuring it's issued by a trusted CA.

  2. The client verifies that the certificate's domain and any additional domains cover the host of the current request.

If both checks pass, the server is deemed trustworthy; otherwise, the connection should be terminated.

When HTTPDNS resolves a domain name, the host in the request URL is replaced with the IP address from HTTPDNS. This can lead to a domain mismatch during certificate verification, causing the SSL/TLS handshake to fail.

SNI

SNI (Server Name Indication) is an SSL/TLS extension that allows a server to host multiple domains and certificates. It operates as follows:

  1. The client sends the domain name of the requested site before establishing an SSL connection.

  2. The server responds with the appropriate certificate for the given domain name.

Most operating systems and browsers support SNI, which is also included in OpenSSL 0.9.8 and later.

When HTTPDNS is used, the server receives the resolved IP address instead of the domain name, which may result in the server returning a default or no certificate, leading to handshake failure.

Note

For instance, when accessing resources on a CDN-accelerated site via HTTPS, the site may serve multiple domains, necessitating the use of SNI to specify the correct certificate for the domain.

HTTPS scenarios (non-SNI) solution

To address the domain mismatch issue, the following solution can be applied: Intercept the certificate verification process, substitute the IP address with the original domain name, and proceed with the verification.

Important

Should a network request utilizing this solution encounter an SSL verification error, for instance, the Android system error System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found., verify if the scenario includes SNI (multiple HTTPS domain names on a single IP).

The following example is applicable to the HttpURLConnection interface.

try {
    val url = "https://140.205.XX.XX/?sprefer=sypc00"
    val connection = URL(url).openConnection() as HttpURLConnection
    connection.setRequestProperty("Host", "m.taobao.com")
    if (connection is HttpsURLConnection) {
        connection.hostnameVerifier = HostnameVerifier { _, session ->
            /*
            * Description about this interface is provided in the official documentation:
            * This is an extended verification option that implementers can provide.
            * It is to be used during a handshake if the URL's hostname does not match the
            * peer's identification hostname.
            *
            * After using HTTPDNS, the hostname set in the URL is not the hostname of the remote host (e.g., m.taobao.com), and does not match the domain bound to the certificate.
            * Android HttpsURLConnection provides a callback interface for users to handle such customized scenarios.
            * After confirming that the origin IP address returned by HTTPDNS matches the IP address carried by the session, you can use the callback method to replace the domain name to be verified with the original real domain name for verification.
            *
            */
            HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session)
        }
    }
    connection.connect()
} catch (e: java.lang.Exception) {
    e.printStackTrace()
}
try {
    String url = "https://140.205.XX.XX/?sprefer=sypc00";
    HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();

    connection.setRequestProperty("Host", "m.taobao.com");

    if (connection instanceof HttpsURLConnection) {
        connection.setHostnameVerifier(new HostnameVerifier() {
    
           /*
            * Description about this interface is provided in the official documentation:
            * This is an extended verification option that implementers can provide.
            * It is to be used during a handshake if the URL's hostname does not match the
            * peer's identification hostname.
            *
            * After using HTTPDNS, the hostname set in the URL is not the hostname of the remote host (e.g., m.taobao.com), and does not match the domain bound to the certificate.
            * Android HttpsURLConnection provides a callback interface for users to handle such customized scenarios.
            * After confirming that the origin IP address returned by HTTPDNS matches the IP address carried by the session, you can use the callback method to replace the domain name to be verified with the original real domain name for verification.
            *
            */
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
            }
        });
    }

    connection.connect();
} catch (Exception e) {
    e.printStackTrace();
}

HTTPS scenarios (SNI) solution

The HTTPDNS Android Demo provides sample code for implementing HTTPDNS in SNI scenarios using the HttpsURLConnection interface.

Customize the SSLSocketFactory to replace the domain name with the IP address from HTTPDNS during socket creation and configure SNI/HostName verification.

class TlsSniSocketFactory constructor(conn: HttpsURLConnection): SSLSocketFactory() {
    private val mConn: HttpsURLConnection

    init {
        mConn = conn
    }

    override fun createSocket(plainSocket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket {
        var peerHost = mConn.getRequestProperty("Host")
        if (peerHost == null) {
            peerHost = host
        }

        val address = plainSocket!!.inetAddress
        if (autoClose) {
            // we don't need the plainSocket
            plainSocket.close()
        }

        // create and connect SSL socket, but don't do hostname/certificate verification yet
        val sslSocketFactory =
            SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
        val ssl = sslSocketFactory.createSocket(address, R.attr.port) as SSLSocket

        // enable TLSv1.1/1.2 if available
        ssl.enabledProtocols = ssl.supportedProtocols

        // set up SNI before the handshake
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // setting sni hostname
            sslSocketFactory.setHostname(ssl, peerHost)
        } else {
            // No documented SNI support on Android <4.2, trying with reflection
            try {
                val setHostnameMethod = ssl.javaClass.getMethod(
                    "setHostname",
                    String::class.java
                )
                setHostnameMethod.invoke(ssl, peerHost)
            } catch (e: Exception) {
            }
        }

        // verify hostname and certificate
        val session = ssl.session

        if (!HttpsURLConnection.getDefaultHostnameVerifier()
                .verify(peerHost, session)
        ) throw SSLPeerUnverifiedException(
            "Cannot verify hostname: $peerHost"
        )

        return ssl
    }
}
public class TlsSniSocketFactory extends SSLSocketFactory {
    
    private HttpsURLConnection mConn;
    public TlsSniSocketFactory(HttpsURLConnection conn) {
        mConn = conn;
    }

    @Override
    public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
        String peerHost = mConn.getRequestProperty("Host");
        if (peerHost == null)
            peerHost = host;

        InetAddress address = plainSocket.getInetAddress();
        if (autoClose) {
            // we don't need the plainSocket
            plainSocket.close();
        }
        // create and connect SSL socket, but don't do hostname/certificate verification yet
        SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
        SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);

        // enable TLSv1.1/1.2 if available
        ssl.setEnabledProtocols(ssl.getSupportedProtocols());

        // set up SNI before the handshake
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // setting sni hostname
            sslSocketFactory.setHostname(ssl, peerHost);
        } else {
            // No documented SNI support on Android <4.2, trying with reflection
            try {
                java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
                setHostnameMethod.invoke(ssl, peerHost);
            } catch (Exception e) {

            }
        }

        // verify hostname and certificate
        SSLSession session = ssl.getSession();

        if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(peerHost, session))
            throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);

        return ssl;
    }
}

For sites requiring SNI, requests often need redirection. The example also includes a method for handling such redirected requests.

fun recursiveRequest(path: String) {
    var conn: HttpURLConnection? = null
    try {
        val url = URL(path)
        conn = url.openConnection() as HttpURLConnection
        // Obtain the IP address by calling the synchronization API
        val httpdnsResult = HttpDns.getService(context, accountID)
            .getHttpDnsResultForHostSync(url.host, RequestIpType.both)
        var ip: String? = null
        if (httpdnsResult.ips != null && httpdnsResult.ips.isNotEmpty()) {
            ip = httpdnsResult.ips[0]
        } else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.isNotEmpty()) {
            ip = httpdnsResult.ipv6s[0]
        }
        if (!TextUtils.isEmpty(ip)) {
            // After the IP address is obtained by using HTTPDNS, replace the value of the HOST field of the HTTP request header with the resolved IP address
            val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
            conn = URL(newUrl).openConnection() as HttpURLConnection
            conn.connectTimeout = 30000
            conn.readTimeout = 30000
            conn.instanceFollowRedirects = false

            // Set the Host header field of the HTTP request
            conn.setRequestProperty("Host", url.host)
            if (conn is HttpsURLConnection) {
                val httpsURLConnection = conn

                // Verify the certificate for an HTTPS request
                httpsURLConnection.hostnameVerifier =
                    HostnameVerifier { _, session ->
                        var host = httpsURLConnection.getRequestProperty("Host")
                        if (null == host) {
                            host = httpsURLConnection.url.host
                        }
                        HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
                    }

                // Handle HTTPS requests with the SNI Create an SSLScoket
                httpsURLConnection.sslSocketFactory = TlsSniSocketFactory(httpsURLConnection)
            }
        }
        val code = conn.responseCode // Network block
        if (code in 300..399) {
            var location = conn.getHeaderField("Location")
            if (location == null) {
                location = conn.getHeaderField("location")
            }
            if (location != null) {
                if (!(location.startsWith("http://") || location
                        .startsWith("https://"))
                ) {
                    // In some cases, the host is omitted, and only the path is returned. Therefore, you need to complete the URL
                    val originalUrl = URL(path)
                    location = (originalUrl.protocol + "://"
                            + originalUrl.host + location)
                }
                recursiveRequest(location)
            }
        } else {
            // Redirect finish.
            val dis = DataInputStream(conn.inputStream)
            var len: Int
            val buff = ByteArray(4096)
            val response = StringBuilder()
            while (dis.read(buff).also { len = it } != -1) {
                response.append(String(buff, 0, len))
            }
            Log.d(TAG, "Response: $response")
        }
    } catch (e: MalformedURLException) {
        Log.w(TAG, "recursiveRequest MalformedURLException")
    } catch (e: IOException) {
        Log.w(TAG, "recursiveRequest IOException")
    } catch (e: java.lang.Exception) {
        Log.w(TAG, "Unknown exception")
    } finally {
        conn?.disconnect()
    }
}
public void recursiveRequest(String path) {
    HttpURLConnection conn = null;
    try {
        URL url = new URL(path);
        conn = (HttpURLConnection) url.openConnection();
        // Obtain the IP address by calling the synchronization API
        HTTPDNSResult httpdnsResult = HttpDns.getService(context, accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);

        String ip = null;
        if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
            ip = httpdnsResult.getIps()[0];
        } else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
            ip = httpdnsResult.getIpv6s()[0];
        }

        if (!TextUtils.isEmpty(ip)) {
            // After the IP address is obtained by using HTTPDNS, replace the value of the HOST field of the HTTP request header with the resolved IP address
            String newUrl = path.replaceFirst(url.getHost(), ip);
            conn = (HttpURLConnection) new URL(newUrl).openConnection();

            conn.setConnectTimeout(30000);
            conn.setReadTimeout(30000);
            conn.setInstanceFollowRedirects(false);

            // Set the Host header field of the HTTP request
            conn.setRequestProperty("Host", url.getHost());

            if (conn instanceof HttpsURLConnection) {
                final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn;

                // Verify the certificate for an HTTPS request
                httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        String host = httpsURLConnection.getRequestProperty("Host");
                        if (null == host) {
                            host = httpsURLConnection.getURL().getHost();
                        }
                        return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
                    }
                });

                // Handle HTTPS requests with the SNI Create an SSLScoket
                httpsURLConnection.setSSLSocketFactory(new TlsSniSocketFactory(httpsURLConnection));
            }
        }

        int code = conn.getResponseCode();// Network block
        if (code >= 300 && code < 400) {
            String location = conn.getHeaderField("Location");
            if (location == null) {
                location = conn.getHeaderField("location");
            }

            if (location != null) {
                if (!(location.startsWith("http://") || location
                        .startsWith("https://"))) {
                    // In some cases, the host is omitted, and only the path is returned. Therefore, you need to complete the URL
                    URL originalUrl = new URL(path);
                    location = originalUrl.getProtocol() + "://"
                            + originalUrl.getHost() + location;
                }

                recursiveRequest(location);
            }
        } else {
            // Redirect finish.
            DataInputStream dis = new DataInputStream(conn.getInputStream());
            int len;
            byte[] buff = new byte[4096];
            StringBuilder response = new StringBuilder();
            while ((len = dis.read(buff)) != -1) {
                response.append(new String(buff, 0, len));
            }
            Log.d(TAG, "Response: " + response.toString());
        }
    } catch (MalformedURLException e) {
        Log.w(TAG, "recursiveRequest MalformedURLException");
    } catch (IOException e) {
        Log.w(TAG, "recursiveRequest IOException");
    } catch (Exception e) {
        Log.w(TAG, "Unknown exception");
    } finally {
        if (conn != null) {
            conn.disconnect();
        }
    }
}