Question

I need to reliably detect if a device has full internet access, i.e. that the user is not confined to a captive portal (also called walled garden), i.e. a limited subnet which forces users to submit their credentials on a form in order to get full access.

My app is automating the authentication process, and therefore it is important to know that full internet access is not available before starting the logon activity.

The question is not about how to check that the network interface is up and in a connected state. It is about making sure the device has unrestricted internet access as opposed to a sandboxed intranet segment.

All the approaches I have tried so far are failing, because connecting to any well-known host would not throw an exception but return a valid HTTP 200 response code because all requests are routed to the login page.

Here are all the approaches I tried but they all return true instead of false for the reasons explained above:

1:

InetAddress.getByName(host).isReachable(TIMEOUT_IN_MILLISECONDS);
isConnected = true; <exception not thrown>

2:

Socket socket = new Socket();
SocketAddress sockaddr = new InetSocketAddress(InetAddress.getByName(host), 80);
socket.connect(sockaddr, pingTimeout);
isConnected = socket.isConnected();

3:

URL url = new URL(hostUrl));
URLConnection urlConn = url.openConnection();
HttpURLConnection httpConn = (HttpURLConnection) urlConn;
httpConn.setAllowUserInteraction(false);
httpConn.setRequestMethod("GET");
httpConn.connect();
responseCode = httpConn.getResponseCode();
isConnected = responseCode == HttpURLConnection.HTTP_OK;

So, how do I make sure I connected to an actual host instead of the login redirection page? Obviously, I could check the actual response body from the 'ping' host I use but it does not look like a proper solution.

Was it helpful?

Solution

For reference, here is the 'official' method from the Android 4.0.1 AOSP code base: WifiWatchdogStateMachine.isWalledGardenConnection(). I am including the code below just in case the link breaks in the future.

private static final String mWalledGardenUrl = "http://clients3.google.com/generate_204";
private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000;

private boolean isWalledGardenConnection() {
    HttpURLConnection urlConnection = null;
    try {
        URL url = new URL(mWalledGardenUrl); // "http://clients3.google.com/generate_204"
        urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setInstanceFollowRedirects(false);
        urlConnection.setConnectTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
        urlConnection.setReadTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
        urlConnection.setUseCaches(false);
        urlConnection.getInputStream();
        // We got a valid response, but not from the real google
        return urlConnection.getResponseCode() != 204;
    } catch (IOException e) {
        if (DBG) {
            log("Walled garden check - probably not a portal: exception "
                    + e);
        }
        return false;
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
    }
}

This approach relies on a specific URL, mWalledGardenUrl = "http://clients3.google.com/generate_204" always returning a 204 response code. This will work even if DNS has been interfered with since in that case a 200 code will be returned instead of the expected 204. I have seen some captive portals spoofing requests to this specific URL in order to prevent the Internet not accessible message on Android devices.

Google has a variation of this theme: fetching http://www.google.com/blank.html will return a 200 code with a zero-length response body. So if you get a non-empty body this would be another way to figure out that you are behind a walled garden.

Apple has its own URLs for detecting captive portals: when network is up IOS and MacOS devices would connect to an URL like http://www.apple.com/library/test/success.html, http://attwifi.apple.com/library/test/success.html, or http://captive.apple.com/hotspot-detect.html which must return an HTTP status code of 200 and a body containing Success.

NOTE: This approach will not work in areas with restricted Internet access such as China where the whole country is a walled garden, and where some Google/Apple services might be blocked. Some of these might not be blocked: http://www.google.cn/generate_204, http://g.cn/generate_204, http://gstatic.com/generate_204 or http://connectivitycheck.gstatic.com/generate_204 — yet these all belong to google so not guaranteed to work.

OTHER TIPS

Another possible solution might be to connect via HTTPS and inspect the target certificate. Not sure if walled gardens actually serve the login page via HTTPS or just drop the connections. In either case, you should be able to see that your destination is not the one you expected.

Of course, you also have the overhead of TLS and certificate checks. Such is the price of authenticated connections, unfortunately.

I believe preventing redirection for your connection will work.

URL url = new URL(hostUrl));
HttpURLConnection httpConn = (HttpURLConnection)url.openConnection();

/* This line prevents redirects */
httpConn.setInstanceFollowRedirects( false );

httpConn.setAllowUserInteraction( false );
httpConn.setRequestMethod( "GET" );
httpConn.connect();
responseCode = httpConn.getResponseCode();
isConnected = responseCode == HttpURLConnection.HTTP_OK;

If that doesn't work, then I think the only way to do it is to check the body of the response.

This has been implemented on Android 4.2.2+ version - I find their approach fast and interesting :

CaptivePortalTracker.java detects walled garden as follows - Try to connect to www.google.com/generate_204 - Check that the HTTP response is 204

If the check fails, we are in a walled garden.

private boolean isCaptivePortal(InetAddress server) {
    HttpURLConnection urlConnection = null;
    if (!mIsCaptivePortalCheckEnabled) return false;

    mUrl = "http://" + server.getHostAddress() + "/generate_204";
    if (DBG) log("Checking " + mUrl);
    try {
        URL url = new URL(mUrl);
        urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setInstanceFollowRedirects(false);
        urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
        urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
        urlConnection.setUseCaches(false);
        urlConnection.getInputStream();
        // we got a valid response, but not from the real google
        return urlConnection.getResponseCode() != 204;
    } catch (IOException e) {
        if (DBG) log("Probably not a portal: exception " + e);
        return false;
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
    }
}

if you are already using retrofit you can do it by retrofit. just make a ping.html page and send an head request to it using retrofit and make sure your http client is configured like below: (followRedirects(false) part is the most important part)

private OkHttpClient getCheckInternetOkHttpClient() {
    return new OkHttpClient.Builder()
            .readTimeout(2L, TimeUnit.SECONDS)
            .connectTimeout(2L, TimeUnit.SECONDS)
            .followRedirects(false)
            .build();
}

then build your retrofit like below:

private InternetCheckApi getCheckInternetRetrofitApi() {
    return (new Retrofit.Builder())
            .baseUrl("[base url of your ping.html page]")             
            .addConverterFactory(GsonConverterFactory.create(new Gson()))
            .client(getCheckInternetOkHttpClient())
            .build().create(InternetCheckApi.class);
}

your InternetCheckApi.class would be like:

public interface InternetCheckApi {
    @Headers({"Content-Typel: application/json"})
    @HEAD("ping.html")
    Call<Void> checkInternetConnectivity();
}

then you can use it like below:

getCheckInternetOkHttpClient().checkInternetConnectivity().enqueue(new Callback<Void>() {
     public void onResponse(Call<Void> call, Response<Void> response) {
       if(response.code() == 200) {
        //internet is available
       } else {
         //internet is not available
       }
     }

     public void onFailure(Call<Void> call, Throwable t) {
        //internet is not available
     }
  }
);

note that your internet check http client must be separate from your main http client.

This is best done here as in AOSP : https://github.com/aosp-mirror/platform_frameworks_base/blob/6bebb8418ceecf44d2af40033870f3aabacfe36e/core/java/android/net/captiveportal/CaptivePortalProbeResult.java#L61

https://github.com/aosp-mirror/platform_frameworks_base/blob/e3a0f42e8e8678f6d90ddf104d485858fbb2e35b/services/core/java/com/android/server/connectivity/NetworkMonitor.java

private static final String GOOGLE_PING_URL = "http://google.com/generate_204";
private static final int SOCKET_TIMEOUT_MS = 10000;

public boolean isCaptivePortal () {

try {
            URL url = new URL(GOOGLE_PING_URL);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setUseCaches(false);
            urlConnection.getInputStream();
            return (urlConnection.getResponseCode() != 204)
                    && (urlConnection.getResponseCode() >= 200)
                    && (urlConnection.getResponseCode() <= 399);
        } catch (Exception e) {
            // for any exception throw an exception saying check was unsuccesful
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
}

Please note this will probably not work on a proxy network and something more advanced as in the AOSP url needs to be done

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top