Retrofit request throwing UnknownHostException behind VPN

3.4k Views Asked by At

So I'm having an issue that looks that it came straight out from the Twilight Zone.

Problem

I have to hit a REST API endpoint from a backend, the thing is that in order to hit that endpoint I need to go through a VPN. Otherwise the host is not reachable. On desktop everything works fine, I open Postman, hit the GET endpoint and get the response. However when I try to hit the same endpoint through my Android device Retrofit throws an UnknownHostException.

Context

The endpoint url is something like https://api.something.something.net/. I'm using dependency injection with Dagger, so I've a NetworkModule that looks like:

...
NetworkModule("https://api.something.something.net/")
...
@Module
class NetworkModule(
    private val baseHost: String
) {
...
    @Provides
    @Named("authInterceptor")
    fun providesAuthInterceptor(
        @Named("authToken") authToken: String
    ): Interceptor {
        return Interceptor { chain ->
            var request = chain.request()

            request = request.newBuilder()
                .addHeader("Content-Type", "application/json")
                .addHeader("Authorization", "Bearer $authToken")
                .build()
            val response = chain.proceed(request)
        }
    }
...
    @Provides
    @Singleton
    fun provideOkHttpClient(
        curlInterceptor: CurlInterceptor,
        @Named("authInterceptor") authInterceptor: Interceptor
    ): OkHttpClient {
        val builder = OkHttpClient.Builder()
        builder.addInterceptor(authInterceptor)
        builder.addInterceptor(curlInterceptor)
        return builder.build()
    }
...
    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit {
        return Retrofit.Builder()
                .baseUrl(baseHost)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(okHttpClient)
                .build()
    }
}

Then I've a bunch of Repositories which are the ones doing the request through Retrofit:

class MyApiRepositoryImpl(
    val myRetrofitApi: MyRetrofitApi,
    val uiScheduler: Scheduler,
    val backgroundScheduler: Scheduler
) : MyApiRepository {

    override fun getSomethingFromTheApi(): Observable<DataResource<List<ApiSomethingResponse>>> {
        return myRetrofitApi.getResponseFromEndpoint()
            .map {
                if (it.isSuccessful) {
                    DataResource(it.body()?.list!!, ResourceType.NETWORK_SUCCESS)
                } else {
                    throw RuntimeException("Network request failed code ${it.code()}")
                }
            }
            .subscribeOn(backgroundScheduler)
            .observeOn(uiScheduler)
            .toObservable()
    }
}

And this is the Retrofit's API interface:

interface MyRetrofitApi {

    @GET("/v1/something/")
    fun getResponseFromEndpoint(): Single<Response<ApiSomethingResponse>>
}

So, when I call this Repository method from my Interactor/UseCases it jumps straight through the onError and shows the UnknownHostException.

What I tried so far

  • I switched Retrofit by Volley and later by Ion, just to be sure that wasn't something related to the rest client. I got the same exception in all cases:

java.net.UnknownHostException: Unable to resolve host "api.something.something.net": No address associated with hostname

com.android.volley.NoConnectionError: java.net.UnknownHostException: Unable to resolve host "api.something.something.net": No address associated with hostname

I tried every configuration possible with Retrofit and the OkHttpClient:

  • On OkHttpClient I tried setting the followSslRedirects to true and false. followRedirects to true and false. Set hostnameVerifier to allow any hostname to pass through. Set a SSLSocketFactory to allow any unsigned certificates to pass through.

  • On my Manifest I set my android:networkSecurityConfig to:

https://github.com/commonsguy/cwac-netsecurity/issues/5

  • I tested the App on my Android Device (Android Nougat), on Emulators with Nougat, Marshmellow and Oreo, and a Genymotion emulator with Nougat.

  • I tried hitting a random public endpoint (https://jsonplaceholder.typicode.com/todos/1) and It worked perfectly. So this isn't an issue with the internet connection.

  • I've these three permissions set on my Manifest:

    android.permission.INTERNET 
    android.permission.ACCESS_NETWORK_STATE
    android.permission.ACCESS_WIFI_STATE
    

It's super weird because I've an Interceptor set to convert all the requests into cURL requests, I copied and pasted the same request that is failing into Postman and works perfectly.

  • On my laptop I'm using Cisco AnyConnect, on my Android device I'm using the Cisco AnyConnect App and AFAIK on the emulators and Genymotion it should use the same network than the hosting machine.

There's a couple of websites that are only visible through the VPN and I can see them on the devices and on the emulators. But the endpoint URL is still unreachable from the App.

Couple of weird things

Yes, this gets weirder. If I hit the endpoint through the Chrome browser in my device or in the emulator I got redirected into a login page, that's because I'm not sending the authorization token. Now If I check the network responses through chrome://inspect I can get the IP of the host. Now if I change the base url of the NetworkModule by the IP and I add this line to the authorization Interceptor:

@Provides
@Named("authInterceptor")
fun providesAuthInterceptor(
    @Named("authToken") authToken: String
): Interceptor {
    return Interceptor { chain ->
        var request = chain.request()

        request = request.newBuilder()
            .addHeader("Content-Type", "application/json")
            .addHeader("Authorization", "Bearer $authToken")
            .build()
        val response = chain.proceed(request)
        response.newBuilder().code(HttpURLConnection.HTTP_SEE_OTHER).build() // <--- This one
    }
}

Then I start getting:

11-13 13:56:01.084 4867-4867/com.something.myapp D/GetSomethingUseCase: javax.net.ssl.SSLPeerUnverifiedException: Hostname <BASE IP> not verified:
        certificate: sha256/someShaString
        DN: CN=something.server.net,OU=Title,O=Company Something\, LLC,L=Some City,ST=SomeState,C=SomeCountryCode
        subjectAltNames: [someServer1.com, someServer2.com, someServer3.com, someServer4.com, andSoOn.com]

I'm not sure if this is unrelated or if it's actually one step forward to fix it.

Any tip or advice is appreciated.

Thanks,

2

There are 2 best solutions below

7
On

I think this issue may be related to ssl certificate validity. for dealing with that issue you should to set setSSLSocketFactory in HttpClient.

  private SSLConnectionSocketFactory getSSLSocketFactory() {
  KeyStore trustStore;
  try {
    trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(null, null);
    TrustStrategy trustStrategy = new TrustStrategy() {
        @Override
        public boolean isTrusted(X509Certificate[] chain, String authType) 
    throws CertificateException {
            return true;
        }

    };

    SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
    sslContextBuilder.loadTrustMaterial(trustStore, trustStrategy);
    sslContextBuilder.useTLS();
    SSLContext sslContext = sslContextBuilder.build();
    SSLConnectionSocketFactory sslSocketFactory = new 
    SSLConnectionSocketFactory(sslContext);
    return sslSocketFactory;
       } catch (GeneralSecurityException | IOException e) {
    System.err.println("SSL Error : " + e.getMessage());
      }
   return null;
  }

 HttpClientBuilder.create().setSSLSocketFactory(getSSLSocketFactory())
 .build();
0
On