/* * Copyright (c) 2015, Psiphon Inc. * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package ca.psiphon; import android.annotation.TargetApi; import android.content.Context; import android.net.ConnectivityManager; import android.net.wifi.WifiManager; import android.net.wifi.WifiInfo; import android.net.LinkProperties; import android.net.NetworkInfo; import android.net.VpnService; import android.os.Build; import android.os.ParcelFileDescriptor; import android.telephony.TelephonyManager; import android.util.Base64; import org.apache.http.conn.util.InetAddressUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import go.psi.Psi; public class PsiphonTunnel extends Psi.PsiphonProvider.Stub { public interface HostService { public String getAppName(); public Context getContext(); public Object getVpnService(); // Object must be a VpnService (Android < 4 cannot reference this class name) public Object newVpnServiceBuilder(); // Object must be a VpnService.Builder (Android < 4 cannot reference this class name) public String getPsiphonConfig(); public void onDiagnosticMessage(String message); public void onAvailableEgressRegions(List regions); public void onSocksProxyPortInUse(int port); public void onHttpProxyPortInUse(int port); public void onListeningSocksProxyPort(int port); public void onListeningHttpProxyPort(int port); public void onUpstreamProxyError(String message); public void onConnecting(); public void onConnected(); public void onHomepage(String url); public void onClientRegion(String region); public void onClientUpgradeDownloaded(String filename); public void onClientIsLatestVersion(); public void onSplitTunnelRegion(String region); public void onUntunneledAddress(String address); public void onBytesTransferred(long sent, long received); public void onStartedWaitingForNetworkConnectivity(); public void onActiveAuthorizationIDs(List authorizations); public void onExiting(); } private final HostService mHostService; private AtomicBoolean mVpnMode; private PrivateAddress mPrivateAddress; private AtomicReference mTunFd; private AtomicInteger mLocalSocksProxyPort; private AtomicBoolean mRoutingThroughTunnel; private Thread mTun2SocksThread; private AtomicBoolean mIsWaitingForNetworkConnectivity; private AtomicReference mClientPlatformPrefix; private AtomicReference mClientPlatformSuffix; // mUsePacketTunnel specifies whether to use the packet // tunnel instead of tun2socks; currently this is for // testing only and is disabled. private boolean mUsePacketTunnel = false; // Only one PsiphonVpn instance may exist at a time, as the underlying // go.psi.Psi and tun2socks implementations each contain global state. private static PsiphonTunnel mPsiphonTunnel; public static synchronized PsiphonTunnel newPsiphonTunnel(HostService hostService) { if (mPsiphonTunnel != null) { mPsiphonTunnel.stop(); } // Load the native go code embedded in psi.aar System.loadLibrary("gojni"); mPsiphonTunnel = new PsiphonTunnel(hostService); return mPsiphonTunnel; } private PsiphonTunnel(HostService hostService) { mHostService = hostService; mVpnMode = new AtomicBoolean(false); mTunFd = new AtomicReference(); mLocalSocksProxyPort = new AtomicInteger(0); mRoutingThroughTunnel = new AtomicBoolean(false); mIsWaitingForNetworkConnectivity = new AtomicBoolean(false); mClientPlatformPrefix = new AtomicReference(""); mClientPlatformSuffix = new AtomicReference(""); } public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } //---------------------------------------------------------------------------------------------- // Public API //---------------------------------------------------------------------------------------------- // To start, call in sequence: startRouting(), then startTunneling(). After startRouting() // succeeds, the caller must call stop() to clean up. These functions should not be called // concurrently. Do not call stop() while startRouting() or startTunneling() is in progress. // Returns true when the VPN routing is established; returns false if the VPN could not // be started due to lack of prepare or revoked permissions (called should re-prepare and // try again); throws exception for other error conditions. public synchronized boolean startRouting() throws Exception { // Note: tun2socks is loaded even in mUsePacketTunnel mode, // as disableUdpGwKeepalive will still be called. // Load tun2socks library embedded in the aar // If this method is called more than once with the same library name, the second and subsequent calls are ignored. // http://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html#loadLibrary%28java.lang.String%29 System.loadLibrary("tun2socks"); return startVpn(); } // Throws an exception in error conditions. In the case of an exception, the routing // started by startRouting() is not immediately torn down (this allows the caller to control // exactly when VPN routing is stopped); caller should call stop() to clean up. public synchronized void startTunneling(String embeddedServerEntries) throws Exception { startPsiphon(embeddedServerEntries); } // Note: to avoid deadlock, do not call directly from a HostService callback; // instead post to a Handler if necessary to trigger from a HostService callback. // For example, deadlock can occur when a Notice callback invokes stop() since stop() calls // Psi.Stop() which will block waiting for tunnel-core Controller to shutdown which in turn // waits for Notice callback invoker to stop, meanwhile the callback thread has blocked waiting // for stop(). public synchronized void stop() { stopVpn(); stopPsiphon(); mVpnMode.set(false); mLocalSocksProxyPort.set(0); } // Note: same deadlock note as stop(). public synchronized void restartPsiphon() throws Exception { stopPsiphon(); startPsiphon(""); } public void setClientPlatformAffixes(String prefix, String suffix) { mClientPlatformPrefix.set(prefix); mClientPlatformSuffix.set(suffix); } // Writes Go runtime profile information to a set of files in the specifiec output directory. // cpuSampleDurationSeconds and blockSampleDurationSeconds determines how to long to wait and // sample profiles that require active sampling. When set to 0, these profiles are skipped. public void writeRuntimeProfiles(String outputDirectory, int cpuSampleDurationSeconnds, int blockSampleDurationSeconds) { Psi.WriteRuntimeProfiles(outputDirectory, cpuSampleDurationSeconnds, blockSampleDurationSeconds); } //---------------------------------------------------------------------------------------------- // VPN Routing //---------------------------------------------------------------------------------------------- private final static String VPN_INTERFACE_NETMASK = "255.255.255.0"; private final static int VPN_INTERFACE_MTU = 1500; private final static int UDPGW_SERVER_PORT = 7300; private final static String DEFAULT_PRIMARY_DNS_SERVER = "8.8.4.4"; private final static String DEFAULT_SECONDARY_DNS_SERVER = "8.8.8.8"; // Note: Atomic variables used for getting/setting local proxy port, routing flag, and // tun fd, as these functions may be called via PsiphonProvider callbacks. Do not use // synchronized functions as stop() is synchronized and a deadlock is possible as callbacks // can be called while stop holds the lock. @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private boolean startVpn() throws Exception { mVpnMode.set(true); mPrivateAddress = selectPrivateAddress(); Locale previousLocale = Locale.getDefault(); final String errorMessage = "startVpn failed"; try { // Workaround for https://code.google.com/p/android/issues/detail?id=61096 Locale.setDefault(new Locale("en")); int mtu = VPN_INTERFACE_MTU; String dnsResolver = mPrivateAddress.mRouter; if (mUsePacketTunnel) { mtu = (int)Psi.GetPacketTunnelMTU(); dnsResolver = Psi.GetPacketTunnelDNSResolverIPv4Address(); } ParcelFileDescriptor tunFd = ((VpnService.Builder) mHostService.newVpnServiceBuilder()) .setSession(mHostService.getAppName()) .setMtu(mtu) .addAddress(mPrivateAddress.mIpAddress, mPrivateAddress.mPrefixLength) .addRoute("0.0.0.0", 0) .addRoute(mPrivateAddress.mSubnet, mPrivateAddress.mPrefixLength) .addDnsServer(dnsResolver) .establish(); if (tunFd == null) { // As per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29, // this application is no longer prepared or was revoked. return false; } mTunFd.set(tunFd); mRoutingThroughTunnel.set(false); mHostService.onDiagnosticMessage("VPN established"); } catch(IllegalArgumentException e) { throw new Exception(errorMessage, e); } catch(IllegalStateException e) { throw new Exception(errorMessage, e); } catch(SecurityException e) { throw new Exception(errorMessage, e); } finally { // Restore the original locale. Locale.setDefault(previousLocale); } return true; } private boolean isVpnMode() { return mVpnMode.get(); } private void setLocalSocksProxyPort(int port) { mLocalSocksProxyPort.set(port); } private void routeThroughTunnel() { if (!mRoutingThroughTunnel.compareAndSet(false, true)) { return; } if (!mUsePacketTunnel) { ParcelFileDescriptor tunFd = mTunFd.getAndSet(null); if (tunFd == null) { return; } String socksServerAddress = "127.0.0.1:" + Integer.toString(mLocalSocksProxyPort.get()); String udpgwServerAddress = "127.0.0.1:" + Integer.toString(UDPGW_SERVER_PORT); startTun2Socks( tunFd, VPN_INTERFACE_MTU, mPrivateAddress.mRouter, VPN_INTERFACE_NETMASK, socksServerAddress, udpgwServerAddress, true); } mHostService.onDiagnosticMessage("routing through tunnel"); // TODO: should double-check tunnel routing; see: // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/1dc5e4257dca99790109f3bf374e8ab3a0ead4d7/Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java?at=default#cl-779 } private void stopVpn() { if (!mUsePacketTunnel) { stopTun2Socks(); } ParcelFileDescriptor tunFd = mTunFd.getAndSet(null); if (tunFd != null) { try { tunFd.close(); } catch (IOException e) { } } mRoutingThroughTunnel.set(false); } //---------------------------------------------------------------------------------------------- // PsiphonProvider (Core support) interface implementation //---------------------------------------------------------------------------------------------- @Override public void Notice(String noticeJSON) { handlePsiphonNotice(noticeJSON); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public String BindToDevice(long fileDescriptor) throws Exception { if (!((VpnService)mHostService.getVpnService()).protect((int)fileDescriptor)) { throw new Exception("protect socket failed"); } return ""; } @Override public long HasNetworkConnectivity() { boolean hasConnectivity = hasNetworkConnectivity(mHostService.getContext()); boolean wasWaitingForNetworkConnectivity = mIsWaitingForNetworkConnectivity.getAndSet(!hasConnectivity); if (!hasConnectivity && !wasWaitingForNetworkConnectivity) { // HasNetworkConnectivity may be called many times, but only call // onStartedWaitingForNetworkConnectivity once per loss of connectivity, // so the HostService may log a single message. mHostService.onStartedWaitingForNetworkConnectivity(); } // TODO: change to bool return value once gobind supports that type return hasConnectivity ? 1 : 0; } @Override public String GetPrimaryDnsServer() { String dnsResolver = null; try { dnsResolver = getFirstActiveNetworkDnsResolver(mHostService.getContext()); } catch (Exception e) { mHostService.onDiagnosticMessage("failed to get active network DNS resolver: " + e.getMessage()); dnsResolver = DEFAULT_PRIMARY_DNS_SERVER; } return dnsResolver; } @Override public String GetSecondaryDnsServer() { return DEFAULT_SECONDARY_DNS_SERVER; } @Override public String IPv6Synthesize(String IPv4Addr) { return IPv4Addr; } @Override public String GetNetworkID() { // The network ID contains potential PII. In tunnel-core, the network ID // is used only locally in the client and not sent to the server. // // See network ID requirements here: // https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter String networkID = "UNKNOWN"; Context context = mHostService.getContext(); ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = null; try { activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); } catch (java.lang.Exception e) { // May get exceptions due to missing permissions like android.permission.ACCESS_NETWORK_STATE. // Apps using the Psiphon Library and lacking android.permission.ACCESS_NETWORK_STATE will // proceed and use tactics, but with "UNKNOWN" as the sole network ID. } if (activeNetworkInfo != null && activeNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI) { networkID = "WIFI"; try { WifiManager wifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); if (wifiInfo != null) { String wifiNetworkID = wifiInfo.getBSSID(); if (wifiNetworkID.equals("02:00:00:00:00:00")) { // "02:00:00:00:00:00" is reported when the app does not have the ACCESS_COARSE_LOCATION permission: // https://developer.android.com/about/versions/marshmallow/android-6.0-changes#behavior-hardware-id // The Psiphon client should allow the user to opt-in to this permission. If they decline, fail over // to using the WiFi IP address. wifiNetworkID = String.valueOf(wifiInfo.getIpAddress()); } networkID += "-" + wifiNetworkID; } } catch (java.lang.Exception e) { // May get exceptions due to missing permissions like android.permission.ACCESS_WIFI_STATE. // Fall through and use just "WIFI" } } else if (activeNetworkInfo != null && activeNetworkInfo.getType() == ConnectivityManager.TYPE_MOBILE) { networkID = "MOBILE"; try { TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager != null) { networkID += "-" + telephonyManager.getNetworkOperator(); } } catch (java.lang.Exception e) { // May get exceptions due to missing permissions. // Fall through and use just "MOBILE" } } return networkID; } //---------------------------------------------------------------------------------------------- // Psiphon Tunnel Core //---------------------------------------------------------------------------------------------- private void startPsiphon(String embeddedServerEntries) throws Exception { stopPsiphon(); mHostService.onDiagnosticMessage("starting Psiphon library"); // In packet tunnel mode, Psi.Start will dup the tun file descriptor // passed in via the config. So here we "check out" mTunFd, to ensure // it can't be closed before it's duplicated. (This would only happen // if stop() is called concurrently with startTunneling(), which should // not be done -- this could also cause file descriptor issues in // tun2socks mode. With the "check out", a closed and recycled file // descriptor will not be copied; but a different race condition takes // the place of that one: stop() may fail to close the tun fd. So the // prohibition on concurrent calls remains.) // // In tun2socks mode, the ownership of the fd is transferred to tun2socks. // In packet tunnel mode, tunnel code dups the fd and manages that copy // while PsiphonTunnel retains ownership of the original mTunFd copy. Both // file descriptors must be closed to halt VpnService, and stop() does // this. ParcelFileDescriptor tunFd = null; int fd = -1; if (mUsePacketTunnel) { tunFd = mTunFd.getAndSet(null); if (tunFd != null) { fd = tunFd.getFd(); } } try { Psi.Start( loadPsiphonConfig(mHostService.getContext(), fd), embeddedServerEntries, "", this, isVpnMode(), false // Do not use IPv6 synthesizer for android ); } catch (java.lang.Exception e) { throw new Exception("failed to start Psiphon library", e); } finally { if (mUsePacketTunnel) { mTunFd.getAndSet(tunFd); } } mHostService.onDiagnosticMessage("Psiphon library started"); } private void stopPsiphon() { mHostService.onDiagnosticMessage("stopping Psiphon library"); Psi.Stop(); mHostService.onDiagnosticMessage("Psiphon library stopped"); } private String loadPsiphonConfig(Context context, int tunFd) throws IOException, JSONException { // Load settings from the raw resource JSON config file and // update as necessary. Then write JSON to disk for the Go client. JSONObject json = new JSONObject(mHostService.getPsiphonConfig()); // On Android, this directory must be set to the app private storage area. // The Psiphon library won't be able to use its current working directory // and the standard temporary directories do not exist. if (!json.has("DataStoreDirectory")) { json.put("DataStoreDirectory", context.getFilesDir()); } if (!json.has("RemoteServerListDownloadFilename")) { File remoteServerListDownload = new File(context.getFilesDir(), "remote_server_list"); json.put("RemoteServerListDownloadFilename", remoteServerListDownload.getAbsolutePath()); } File oslDownloadDir = new File(context.getFilesDir(), "osl"); if (!oslDownloadDir.exists() && !oslDownloadDir.mkdirs()) { // Failed to create osl directory // TODO: proceed anyway? throw new IOException("failed to create OSL download directory"); } json.put("ObfuscatedServerListDownloadDirectory", oslDownloadDir.getAbsolutePath()); // Note: onConnecting/onConnected logic assumes 1 tunnel connection json.put("TunnelPoolSize", 1); // Continue to run indefinitely until connected if (!json.has("EstablishTunnelTimeoutSeconds")) { json.put("EstablishTunnelTimeoutSeconds", 0); } // This parameter is for stats reporting if (!json.has("TunnelWholeDevice")) { json.put("TunnelWholeDevice", isVpnMode() ? 1 : 0); } json.put("EmitBytesTransferred", true); if (mLocalSocksProxyPort.get() != 0 && (!json.has("LocalSocksProxyPort") || json.getInt("LocalSocksProxyPort") == 0)) { // When mLocalSocksProxyPort is set, tun2socks is already configured // to use that port value. So we force use of the same port. // A side-effect of this is that changing the SOCKS port preference // has no effect with restartPsiphon(), a full stop() is necessary. json.put("LocalSocksProxyPort", mLocalSocksProxyPort); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { try { json.put( "TrustedCACertificatesFilename", setupTrustedCertificates(mHostService.getContext())); } catch (Exception e) { mHostService.onDiagnosticMessage(e.getMessage()); } } json.put("DeviceRegion", getDeviceRegion(mHostService.getContext())); if (mUsePacketTunnel) { json.put("PacketTunnelTunFileDescriptor", tunFd); json.put("DisableLocalSocksProxy", true); json.put("DisableLocalHTTPProxy", true); } StringBuilder clientPlatform = new StringBuilder(); String prefix = mClientPlatformPrefix.get(); if (prefix.length() > 0) { clientPlatform.append(prefix); } clientPlatform.append("Android_"); clientPlatform.append(Build.VERSION.RELEASE); clientPlatform.append("_"); clientPlatform.append(mHostService.getContext().getPackageName()); String suffix = mClientPlatformSuffix.get(); if (suffix.length() > 0) { clientPlatform.append(suffix); } json.put("ClientPlatform", clientPlatform.toString().replaceAll("[^\\w\\-\\.]", "_")); return json.toString(); } private void handlePsiphonNotice(String noticeJSON) { try { // All notices are sent on as diagnostic messages // except those that may contain private user data. boolean diagnostic = true; JSONObject notice = new JSONObject(noticeJSON); String noticeType = notice.getString("noticeType"); if (noticeType.equals("Tunnels")) { int count = notice.getJSONObject("data").getInt("count"); if (count > 0) { if (isVpnMode()) { routeThroughTunnel(); } mHostService.onConnected(); } else { mHostService.onConnecting(); } } else if (noticeType.equals("AvailableEgressRegions")) { JSONArray egressRegions = notice.getJSONObject("data").getJSONArray("regions"); ArrayList regions = new ArrayList(); for (int i=0; i authorizations = new ArrayList(); for (int i=0; i= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { keyStore = KeyStore.getInstance("AndroidCAStore"); keyStore.load(null, null); } else { keyStore = KeyStore.getInstance("BKS"); FileInputStream inputStream = new FileInputStream("/etc/security/cacerts.bks"); try { keyStore.load(inputStream, "changeit".toCharArray()); } finally { if (inputStream != null) { inputStream.close(); } } } Enumeration aliases = keyStore.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); output.println("-----BEGIN CERTIFICATE-----"); String pemCert = new String(Base64.encode(cert.getEncoded(), Base64.NO_WRAP), "UTF-8"); // OpenSSL appears to reject the default linebreaking done by Base64.encode, // so we manually linebreak every 64 characters for (int i = 0; i < pemCert.length() ; i+= 64) { output.println(pemCert.substring(i, Math.min(i + 64, pemCert.length()))); } output.println("-----END CERTIFICATE-----"); } mHostService.onDiagnosticMessage("prepared PsiphonCAStore"); return file.getAbsolutePath(); } finally { if (output != null) { output.close(); } } } catch (KeyStoreException e) { throw new Exception(errorMessage, e); } catch (NoSuchAlgorithmException e) { throw new Exception(errorMessage, e); } catch (CertificateException e) { throw new Exception(errorMessage, e); } catch (IOException e) { throw new Exception(errorMessage, e); } } private static String getDeviceRegion(Context context) { String region = ""; TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager != null) { region = telephonyManager.getSimCountryIso(); if (region == null) { region = ""; } if (region.length() == 0 && telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { region = telephonyManager.getNetworkCountryIso(); if (region == null) { region = ""; } } } if (region.length() == 0) { Locale defaultLocale = Locale.getDefault(); if (defaultLocale != null) { region = defaultLocale.getCountry(); } } return region.toUpperCase(Locale.US); } //---------------------------------------------------------------------------------------------- // Tun2Socks //---------------------------------------------------------------------------------------------- @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) private void startTun2Socks( final ParcelFileDescriptor vpnInterfaceFileDescriptor, final int vpnInterfaceMTU, final String vpnIpAddress, final String vpnNetMask, final String socksServerAddress, final String udpgwServerAddress, final boolean udpgwTransparentDNS) { if (mTun2SocksThread != null) { return; } mTun2SocksThread = new Thread(new Runnable() { @Override public void run() { runTun2Socks( vpnInterfaceFileDescriptor.detachFd(), vpnInterfaceMTU, vpnIpAddress, vpnNetMask, socksServerAddress, udpgwServerAddress, udpgwTransparentDNS ? 1 : 0); } }); mTun2SocksThread.start(); mHostService.onDiagnosticMessage("tun2socks started"); } private void stopTun2Socks() { if (mTun2SocksThread != null) { try { terminateTun2Socks(); mTun2SocksThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } mTun2SocksThread = null; mHostService.onDiagnosticMessage("tun2socks stopped"); } } public static void logTun2Socks(String level, String channel, String msg) { String logMsg = "tun2socks: " + level + "(" + channel + "): " + msg; mPsiphonTunnel.mHostService.onDiagnosticMessage(logMsg); } private native static int runTun2Socks( int vpnInterfaceFileDescriptor, int vpnInterfaceMTU, String vpnIpAddress, String vpnNetMask, String socksServerAddress, String udpgwServerAddress, int udpgwTransparentDNS); private native static int terminateTun2Socks(); private native static int enableUdpGwKeepalive(); private native static int disableUdpGwKeepalive(); //---------------------------------------------------------------------------------------------- // Implementation: Network Utils //---------------------------------------------------------------------------------------------- private static boolean hasNetworkConnectivity(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivityManager == null) { return false; } NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } private static class PrivateAddress { final public String mIpAddress; final public String mSubnet; final public int mPrefixLength; final public String mRouter; public PrivateAddress(String ipAddress, String subnet, int prefixLength, String router) { mIpAddress = ipAddress; mSubnet = subnet; mPrefixLength = prefixLength; mRouter = router; } } private static PrivateAddress selectPrivateAddress() throws Exception { // Select one of 10.0.0.1, 172.16.0.1, or 192.168.0.1 depending on // which private address range isn't in use. Map candidates = new HashMap(); candidates.put( "10", new PrivateAddress("10.0.0.1", "10.0.0.0", 8, "10.0.0.2")); candidates.put("172", new PrivateAddress("172.16.0.1", "172.16.0.0", 12, "172.16.0.2")); candidates.put("192", new PrivateAddress("192.168.0.1", "192.168.0.0", 16, "192.168.0.2")); candidates.put("169", new PrivateAddress("169.254.1.1", "169.254.1.0", 24, "169.254.1.2")); List netInterfaces; try { netInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); } catch (SocketException e) { throw new Exception("selectPrivateAddress failed", e); } for (NetworkInterface netInterface : netInterfaces) { for (InetAddress inetAddress : Collections.list(netInterface.getInetAddresses())) { String ipAddress = inetAddress.getHostAddress(); if (InetAddressUtils.isIPv4Address(ipAddress)) { if (ipAddress.startsWith("10.")) { candidates.remove("10"); } else if ( ipAddress.length() >= 6 && ipAddress.substring(0, 6).compareTo("172.16") >= 0 && ipAddress.substring(0, 6).compareTo("172.31") <= 0) { candidates.remove("172"); } else if (ipAddress.startsWith("192.168")) { candidates.remove("192"); } } } } if (candidates.size() > 0) { return candidates.values().iterator().next(); } throw new Exception("no private address available"); } public static String getFirstActiveNetworkDnsResolver(Context context) throws Exception { Collection dnsResolvers = getActiveNetworkDnsResolvers(context); if (!dnsResolvers.isEmpty()) { // strip the leading slash e.g., "/192.168.1.1" String dnsResolver = dnsResolvers.iterator().next().toString(); if (dnsResolver.startsWith("/")) { dnsResolver = dnsResolver.substring(1); } return dnsResolver; } throw new Exception("no active network DNS resolver"); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static Collection getActiveNetworkDnsResolvers(Context context) throws Exception { final String errorMessage = "getActiveNetworkDnsResolvers failed"; ArrayList dnsAddresses = new ArrayList(); try { // Hidden API // - only available in Android 4.0+ // - no guarantee will be available beyond 4.2, or on all vendor devices ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); Class LinkPropertiesClass = Class.forName("android.net.LinkProperties"); Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{}); Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager); if (linkProperties != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{}); Collection dnses = (Collection)getDnsesMethod.invoke(linkProperties); for (Object dns : dnses) { dnsAddresses.add((InetAddress)dns); } } else { // LinkProperties is public in API 21 (and the DNS function signature has changed) for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) { dnsAddresses.add(dns); } } } } catch (ClassNotFoundException e) { throw new Exception(errorMessage, e); } catch (NoSuchMethodException e) { throw new Exception(errorMessage, e); } catch (IllegalArgumentException e) { throw new Exception(errorMessage, e); } catch (IllegalAccessException e) { throw new Exception(errorMessage, e); } catch (InvocationTargetException e) { throw new Exception(errorMessage, e); } catch (NullPointerException e) { throw new Exception(errorMessage, e); } return dnsAddresses; } //---------------------------------------------------------------------------------------------- // Exception //---------------------------------------------------------------------------------------------- public static class Exception extends java.lang.Exception { private static final long serialVersionUID = 1L; public Exception(String message) { super(message); } public Exception(String message, Throwable cause) { super(message + ": " + cause.getMessage()); } } }