35

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.

ccpizza
  • 21,405
  • 10
  • 121
  • 123
  • 4
    Since upstream equipment (i.e. a [captive portal](http://en.wikipedia.org/wiki/Captive_portal)) can send anything back down along with an HTTP 200, actually checking the HTTP response body seems the only possible way to 100% guarantee you're reaching the "outside world". Of course, even there the page could be cached...but that's less likely. Common ways around caching issues are to include a spurious HTTP GET parameter in the requested URL (i.e. `?time=1234`). – hall.stephenk Dec 19 '12 at 18:25

6 Answers6

44

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 most Google/Apple services are blocked or filtered. 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.

ccpizza
  • 21,405
  • 10
  • 121
  • 123
  • I'm wondering if urlConnection.getInputStream() will generate unnecessary network traffic. Could we use HttpResponse.getStatusLine().getStatusCode() instead? – Christian Dec 05 '13 at 18:07
  • @Christian: `urlConnection.getInputStream()` is needed in order to actually establish the connection. It will not generate traffic because the stream is not consumed. In any case, if you do actually consume the stream, you will notice that for this specific URL the response body size will be zero length. – ccpizza Dec 07 '13 at 00:17
  • 1
    Sorry for the offtop. Does anybody know how can I emulate captive portal detection for Android if there is no internet access in the hotspot at all? I am using Mikrotik router and created hotspot with DNS record like .* = ROUTER_IP so all domains are redirected to the router. It now popups login page in Windows and iOS but not in Android. I assume something special is needed for Android and how can I make this notification appear without internet? – Taras Apr 25 '15 at 16:39
  • Thanks. Am testing with a pre-pay and have no data plan, and when I jumped off Wifi, or was mobile, I would see this behavior, which I can now detect. – sobelito Dec 07 '15 at 04:43
  • I wonder if this check is needed at all if android already has this built in. According to my experience I can have a BroadcastReceiver listening to ConnectivityManager.CONNECTIVITY_ACTION action will give me indication about the connectivity AFTER it checked the captive portal – Gavriel Feb 23 '16 at 07:54
5

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.

Delyan
  • 8,745
  • 4
  • 35
  • 41
1

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.

Ralgha
  • 7,712
  • 3
  • 32
  • 50
  • 2
    That assumes that the upstream device will redirect you using HTTP. In theory, the device could route you in other ways, i.e. DNS or IP-based. It could also just respond to any HTTP request with a standard page, without ever redirecting you at all. HTTPS should fail in such a case though...maybe that's a possibility as well? – hall.stephenk Dec 19 '12 at 18:27
  • As hall said, if it redirects the request in another way then this method will fail. I deal with this type of gateway quite a bit though since I'm in hotels a lot, and while I've never delved into it to see exactly how it handles the routing, it appears to usually be done with HTTP. – Ralgha Dec 19 '12 at 18:34
0

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();
        }
    }
}
  • 3
    This is just a duplicate of the original answer. The fact that it is from v4.2.2 makes absolutely no change, since the code is the same, and does not add anything new to the original answer. – ccpizza Jul 02 '14 at 16:03
  • I'd like to know why Google changed the code from URL=...clients3.google.com... to the IP-address!? – hgoebl Jul 23 '15 at 10:28
  • Probably found out why they use IP-address: Seems like the host of the `generate_204` resource can be configured. As an example, see http://android.stackexchange.com/a/105611/65578 – hgoebl Jul 23 '15 at 11:00
0

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.

Amir Ziarati
  • 12,070
  • 9
  • 40
  • 50
0

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

vinzzz
  • 1,894
  • 2
  • 11
  • 19