github.com/ooni/psiphon/tunnel-core@v0.0.0-20230105123940-fe12a24c96ee/MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java (about)

     1  /*
     2   * Copyright (c) 2015, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package ca.psiphon;
    21  
    22  import android.annotation.TargetApi;
    23  import android.content.Context;
    24  import android.net.ConnectivityManager;
    25  import android.net.LinkProperties;
    26  import android.net.Network;
    27  import android.net.NetworkCapabilities;
    28  import android.net.NetworkInfo;
    29  import android.net.NetworkRequest;
    30  import android.net.VpnService;
    31  import android.net.wifi.WifiInfo;
    32  import android.net.wifi.WifiManager;
    33  import android.os.Build;
    34  import android.os.ParcelFileDescriptor;
    35  import android.telephony.TelephonyManager;
    36  import android.util.Base64;
    37  
    38  import org.json.JSONArray;
    39  import org.json.JSONException;
    40  import org.json.JSONObject;
    41  
    42  import java.io.File;
    43  import java.io.FileInputStream;
    44  import java.io.FileOutputStream;
    45  import java.io.IOException;
    46  import java.io.PrintStream;
    47  import java.lang.reflect.InvocationTargetException;
    48  import java.lang.reflect.Method;
    49  import java.net.Inet4Address;
    50  import java.net.Inet6Address;
    51  import java.net.InetAddress;
    52  import java.net.NetworkInterface;
    53  import java.net.SocketException;
    54  import java.security.KeyStore;
    55  import java.security.KeyStoreException;
    56  import java.security.NoSuchAlgorithmException;
    57  import java.security.cert.CertificateException;
    58  import java.security.cert.X509Certificate;
    59  import java.util.ArrayList;
    60  import java.util.Collection;
    61  import java.util.Collections;
    62  import java.util.Enumeration;
    63  import java.util.HashMap;
    64  import java.util.List;
    65  import java.util.Locale;
    66  import java.util.Map;
    67  import java.util.concurrent.CountDownLatch;
    68  import java.util.concurrent.atomic.AtomicBoolean;
    69  import java.util.concurrent.atomic.AtomicInteger;
    70  import java.util.concurrent.atomic.AtomicReference;
    71  import java.util.concurrent.Executors;
    72  import java.util.concurrent.ExecutorService;
    73  import java.util.concurrent.Future;
    74  import java.util.concurrent.TimeUnit;
    75  
    76  import psi.Psi;
    77  import psi.PsiphonProvider;
    78  import psi.PsiphonProviderNetwork;
    79  import psi.PsiphonProviderNoticeHandler;
    80  import psi.PsiphonProviderFeedbackHandler;
    81  
    82  public class PsiphonTunnel {
    83  
    84      public interface HostLogger {
    85          default public void onDiagnosticMessage(String message) {}
    86      }
    87  
    88      // Protocol used to communicate the outcome of feedback upload operations to the application
    89      // using PsiphonTunnelFeedback.
    90      public interface HostFeedbackHandler {
    91          // Callback which is invoked once the feedback upload has completed.
    92          // If the exception is non-null, then the upload failed.
    93          default public void sendFeedbackCompleted(java.lang.Exception e) {}
    94      }
    95  
    96      public interface HostLibraryLoader {
    97          default public void loadLibrary(String library) {
    98              System.loadLibrary(library);
    99          }
   100      }
   101  
   102      public interface HostService extends HostLogger, HostLibraryLoader {
   103  
   104          public String getAppName();
   105          public Context getContext();
   106          public String getPsiphonConfig();
   107  
   108          default public Object getVpnService() {return null;} // Object must be a VpnService (Android < 4 cannot reference this class name)
   109          default public Object newVpnServiceBuilder() {return null;} // Object must be a VpnService.Builder (Android < 4 cannot reference this class name)
   110          default public void onAvailableEgressRegions(List<String> regions) {}
   111          default public void onSocksProxyPortInUse(int port) {}
   112          default public void onHttpProxyPortInUse(int port) {}
   113          default public void onListeningSocksProxyPort(int port) {}
   114          default public void onListeningHttpProxyPort(int port) {}
   115          default public void onUpstreamProxyError(String message) {}
   116          default public void onConnecting() {}
   117          default public void onConnected() {}
   118          default public void onHomepage(String url) {}
   119          default public void onClientRegion(String region) {}
   120          default public void onClientAddress(String address) {}
   121          default public void onClientUpgradeDownloaded(String filename) {}
   122          default public void onClientIsLatestVersion() {}
   123          default public void onSplitTunnelRegions(List<String> regions) {}
   124          default public void onUntunneledAddress(String address) {}
   125          default public void onBytesTransferred(long sent, long received) {}
   126          default public void onStartedWaitingForNetworkConnectivity() {}
   127          default public void onStoppedWaitingForNetworkConnectivity() {}
   128          default public void onActiveAuthorizationIDs(List<String> authorizations) {}
   129          default public void onTrafficRateLimits(long upstreamBytesPerSecond, long downstreamBytesPerSecond) {}
   130          default public void onApplicationParameter(String key, Object value) {}
   131          default public void onServerAlert(String reason, String subject, List<String> actionURLs) {}
   132          default public void onExiting() {}
   133      }
   134  
   135      private final HostService mHostService;
   136      private AtomicBoolean mVpnMode;
   137      private PrivateAddress mPrivateAddress;
   138      private AtomicReference<ParcelFileDescriptor> mTunFd;
   139      private AtomicInteger mLocalSocksProxyPort;
   140      private AtomicBoolean mRoutingThroughTunnel;
   141      private Thread mTun2SocksThread;
   142      private AtomicBoolean mIsWaitingForNetworkConnectivity;
   143      private AtomicReference<String> mClientPlatformPrefix;
   144      private AtomicReference<String> mClientPlatformSuffix;
   145      private final boolean mShouldRouteThroughTunnelAutomatically;
   146      private final NetworkMonitor mNetworkMonitor;
   147      private AtomicReference<String> mActiveNetworkType;
   148      private AtomicReference<String> mActiveNetworkDNSServers;
   149  
   150      // Only one PsiphonVpn instance may exist at a time, as the underlying
   151      // psi.Psi and tun2socks implementations each contain global state.
   152      private static PsiphonTunnel mPsiphonTunnel;
   153  
   154      public static synchronized PsiphonTunnel newPsiphonTunnel(HostService hostService) {
   155          return newPsiphonTunnelImpl(hostService, true);
   156      }
   157  
   158      // The two argument override in case the host app wants to take control over calling routeThroughTunnel()
   159      public static synchronized PsiphonTunnel newPsiphonTunnel(HostService hostService, boolean shouldRouteThroughTunnelAutomatically) {
   160          return newPsiphonTunnelImpl(hostService, shouldRouteThroughTunnelAutomatically);
   161      }
   162  
   163      private static PsiphonTunnel newPsiphonTunnelImpl(HostService hostService, boolean shouldRouteThroughTunnelAutomatically) {
   164          if (mPsiphonTunnel != null) {
   165              mPsiphonTunnel.stop();
   166          }
   167          // Load the native go code embedded in psi.aar
   168          hostService.loadLibrary("gojni");
   169          mPsiphonTunnel = new PsiphonTunnel(hostService, shouldRouteThroughTunnelAutomatically);
   170          return mPsiphonTunnel;
   171      }
   172  
   173      // Returns default path where upgrade downloads will be paved. Only applicable if
   174      // DataRootDirectory was not set in the outer config. If DataRootDirectory was set in the
   175      // outer config, use getUpgradeDownloadFilePath with its value instead.
   176      public static String getDefaultUpgradeDownloadFilePath(Context context) {
   177          return Psi.upgradeDownloadFilePath(defaultDataRootDirectory(context).getAbsolutePath());
   178      }
   179  
   180      // Returns the path where upgrade downloads will be paved relative to the configured
   181      // DataRootDirectory.
   182      public static String getUpgradeDownloadFilePath(String dataRootDirectoryPath) {
   183          return Psi.upgradeDownloadFilePath(dataRootDirectoryPath);
   184      }
   185  
   186      private static File defaultDataRootDirectory(Context context) {
   187          return context.getFileStreamPath("ca.psiphon.PsiphonTunnel.tunnel-core");
   188      }
   189  
   190      private PsiphonTunnel(HostService hostService, boolean shouldRouteThroughTunnelAutomatically) {
   191          mHostService = hostService;
   192          mVpnMode = new AtomicBoolean(false);
   193          mTunFd = new AtomicReference<ParcelFileDescriptor>();
   194          mLocalSocksProxyPort = new AtomicInteger(0);
   195          mRoutingThroughTunnel = new AtomicBoolean(false);
   196          mIsWaitingForNetworkConnectivity = new AtomicBoolean(false);
   197          mClientPlatformPrefix = new AtomicReference<String>("");
   198          mClientPlatformSuffix = new AtomicReference<String>("");
   199          mShouldRouteThroughTunnelAutomatically = shouldRouteThroughTunnelAutomatically;
   200          mActiveNetworkType = new AtomicReference<String>("");
   201          mActiveNetworkDNSServers = new AtomicReference<String>("");
   202          mNetworkMonitor = new NetworkMonitor(new NetworkMonitor.NetworkChangeListener() {
   203              @Override
   204              public void onChanged() {
   205                  try {
   206                      reconnectPsiphon();
   207                  } catch (Exception e) {
   208                      mHostService.onDiagnosticMessage("reconnect error: " + e);
   209                  }
   210              }
   211          }, mActiveNetworkType, mActiveNetworkDNSServers, mHostService);
   212      }
   213  
   214      public Object clone() throws CloneNotSupportedException {
   215          throw new CloneNotSupportedException();
   216      }
   217  
   218      //----------------------------------------------------------------------------------------------
   219      // Public API
   220      //----------------------------------------------------------------------------------------------
   221  
   222      // To start, call in sequence: startRouting(), then startTunneling(). After startRouting()
   223      // succeeds, the caller must call stop() to clean up. These functions should not be called
   224      // concurrently. Do not call stop() while startRouting() or startTunneling() is in progress.
   225      // In case the host application requests manual control of routing through tunnel by calling
   226      // PsiphonTunnel.newPsiphonTunnel(HostService hostservice, shouldRouteThroughTunnelAutomatically = false)
   227      // it should also call routeThroughTunnel() at some point, usually after receiving onConnected() callback,
   228      // otherwise it will be called automatically.
   229  
   230      // Returns true when the VPN routing is established; returns false if the VPN could not
   231      // be started due to lack of prepare or revoked permissions (called should re-prepare and
   232      // try again); throws exception for other error conditions.
   233      public synchronized boolean startRouting() throws Exception {
   234          // Load tun2socks library embedded in the aar
   235          // If this method is called more than once with the same library name, the second and subsequent calls are ignored.
   236          // http://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html#loadLibrary%28java.lang.String%29
   237          mHostService.loadLibrary("tun2socks");
   238          return startVpn();
   239      }
   240  
   241      // Starts routing traffic via tunnel by starting tun2socks if it is not running already.
   242      // This will be called automatically right after tunnel gets connected in case the host application
   243      // did not request a manual control over this functionality, see PsiphonTunnel.newPsiphonTunnel
   244      public void routeThroughTunnel() {
   245          if (!mRoutingThroughTunnel.compareAndSet(false, true)) {
   246              return;
   247          }
   248          ParcelFileDescriptor tunFd = mTunFd.getAndSet(null);
   249          if (tunFd == null) {
   250              return;
   251          }
   252  
   253          String socksServerAddress = "127.0.0.1:" + Integer.toString(mLocalSocksProxyPort.get());
   254          String udpgwServerAddress = "127.0.0.1:" + Integer.toString(UDPGW_SERVER_PORT);
   255          startTun2Socks(
   256                  tunFd,
   257                  VPN_INTERFACE_MTU,
   258                  mPrivateAddress.mRouter,
   259                  VPN_INTERFACE_NETMASK,
   260                  socksServerAddress,
   261                  udpgwServerAddress,
   262                  true);
   263  
   264          mHostService.onDiagnosticMessage("routing through tunnel");
   265  
   266          // TODO: should double-check tunnel routing; see:
   267          // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/1dc5e4257dca99790109f3bf374e8ab3a0ead4d7/Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java?at=default#cl-779
   268      }
   269  
   270      // Throws an exception in error conditions. In the case of an exception, the routing
   271      // started by startRouting() is not immediately torn down (this allows the caller to control
   272      // exactly when VPN routing is stopped); caller should call stop() to clean up.
   273      public synchronized void startTunneling(String embeddedServerEntries) throws Exception {
   274          startPsiphon(embeddedServerEntries);
   275      }
   276  
   277      // Note: to avoid deadlock, do not call directly from a HostService callback;
   278      // instead post to a Handler if necessary to trigger from a HostService callback.
   279      // For example, deadlock can occur when a Notice callback invokes stop() since stop() calls
   280      // Psi.stop() which will block waiting for tunnel-core Controller to shutdown which in turn
   281      // waits for Notice callback invoker to stop, meanwhile the callback thread has blocked waiting
   282      // for stop().
   283      public synchronized void stop() {
   284          stopVpn();
   285          stopPsiphon();
   286          mVpnMode.set(false);
   287          mLocalSocksProxyPort.set(0);
   288      }
   289  
   290      // Note: same deadlock note as stop().
   291      public synchronized void restartPsiphon() throws Exception {
   292          stopPsiphon();
   293          startPsiphon("");
   294      }
   295  
   296      public synchronized void reconnectPsiphon() throws Exception {
   297          Psi.reconnectTunnel();
   298      }
   299  
   300      public void setClientPlatformAffixes(String prefix, String suffix) {
   301          mClientPlatformPrefix.set(prefix);
   302          mClientPlatformSuffix.set(suffix);
   303      }
   304  
   305      public String exportExchangePayload() {
   306          return Psi.exportExchangePayload();
   307      }
   308  
   309      public boolean importExchangePayload(String payload) {
   310          return Psi.importExchangePayload(payload);
   311      }
   312  
   313      // Writes Go runtime profile information to a set of files in the specifiec output directory.
   314      // cpuSampleDurationSeconds and blockSampleDurationSeconds determines how to long to wait and
   315      // sample profiles that require active sampling. When set to 0, these profiles are skipped.
   316      public void writeRuntimeProfiles(String outputDirectory, int cpuSampleDurationSeconnds, int blockSampleDurationSeconds) {
   317          Psi.writeRuntimeProfiles(outputDirectory, cpuSampleDurationSeconnds, blockSampleDurationSeconds);
   318      }
   319  
   320      // The interface for managing the Psiphon feedback upload operations.
   321      // Warnings:
   322      // - Should not be used in the same process as PsiphonTunnel.
   323      // - Only a single instance of PsiphonTunnelFeedback should be used at a time. Using multiple
   324      // instances in parallel, or concurrently, will result in undefined behavior.
   325      public static class PsiphonTunnelFeedback {
   326  
   327          final private ExecutorService workQueue;
   328          final private ExecutorService callbackQueue;
   329  
   330          public PsiphonTunnelFeedback() {
   331              workQueue = Executors.newSingleThreadExecutor();
   332              callbackQueue = Executors.newSingleThreadExecutor();
   333          }
   334  
   335          @Override
   336          protected void finalize() throws Throwable {
   337              // Ensure the queues are cleaned up.
   338              shutdownAndAwaitTermination(callbackQueue);
   339              shutdownAndAwaitTermination(workQueue);
   340              super.finalize();
   341          }
   342  
   343          void shutdownAndAwaitTermination(ExecutorService pool) {
   344              try {
   345                  // Wait a while for existing tasks to terminate
   346                  if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
   347                      pool.shutdownNow(); // Cancel currently executing tasks
   348                      // Wait a while for tasks to respond to being cancelled
   349                      if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
   350                          System.err.println("PsiphonTunnelFeedback: pool did not terminate");
   351                          return;
   352                      }
   353                  }
   354              } catch (InterruptedException ie) {
   355                  // (Re-)Cancel if current thread also interrupted
   356                  pool.shutdownNow();
   357                  // Preserve interrupt status
   358                  Thread.currentThread().interrupt();
   359              }
   360          }
   361  
   362          // Upload a feedback package to Psiphon Inc. The app collects feedback and diagnostics
   363          // information in a particular format, then calls this function to upload it for later
   364          // investigation. The feedback compatible config and upload path must be provided by
   365          // Psiphon Inc. This call is asynchronous and returns before the upload completes. The
   366          // operation has completed when sendFeedbackCompleted() is called on the provided
   367          // HostFeedbackHandler. The provided HostLogger will be called to log informational notices,
   368          // including warnings.
   369          //
   370          // Warnings:
   371          // - Only one active upload is supported at a time. An ongoing upload will be cancelled if
   372          // this function is called again before it completes.
   373          // - An ongoing feedback upload started with startSendFeedback() should be stopped with
   374          // stopSendFeedback() before the process exits. This ensures that any underlying resources
   375          // are cleaned up; failing to do so may result in data store corruption or other undefined
   376          // behavior.
   377          // - PsiphonTunnel.startTunneling and startSendFeedback both make an attempt to migrate
   378          // persistent files from legacy locations in a one-time operation. If these functions are
   379          // called in parallel, then there is a chance that the migration attempts could execute at
   380          // the same time and result in non-fatal errors in one, or both, of the migration
   381          // operations.
   382          public void startSendFeedback(Context context, HostFeedbackHandler feedbackHandler, HostLogger logger,
   383                                        String feedbackConfigJson, String diagnosticsJson, String uploadPath,
   384                                        String clientPlatformPrefix, String clientPlatformSuffix) {
   385  
   386              workQueue.submit(new Runnable() {
   387                  @Override
   388                  public void run() {
   389                      try {
   390                          // Adds fields used in feedback upload, e.g. client platform.
   391                          String psiphonConfig = buildPsiphonConfig(context, logger, feedbackConfigJson,
   392                                  clientPlatformPrefix, clientPlatformSuffix, false, 0);
   393  
   394                          Psi.startSendFeedback(psiphonConfig, diagnosticsJson, uploadPath,
   395                                  new PsiphonProviderFeedbackHandler() {
   396                                      @Override
   397                                      public void sendFeedbackCompleted(java.lang.Exception e) {
   398                                          callbackQueue.submit(new Runnable() {
   399                                              @Override
   400                                              public void run() {
   401                                                  feedbackHandler.sendFeedbackCompleted(e);
   402                                              }
   403                                          });
   404                                      }
   405                                  },
   406                                  new PsiphonProviderNetwork() {
   407                                      @Override
   408                                      public long hasNetworkConnectivity() {
   409                                          boolean hasConnectivity = PsiphonTunnel.hasNetworkConnectivity(context);
   410                                          // TODO: change to bool return value once gobind supports that type
   411                                          return hasConnectivity ? 1 : 0;
   412                                      }
   413  
   414                                      @Override
   415                                      public String getNetworkID() {
   416                                          return PsiphonTunnel.getNetworkID(context);
   417                                      }
   418  
   419                                      @Override
   420                                      public String iPv6Synthesize(String IPv4Addr) {
   421                                          // Unused on Android.
   422                                          return PsiphonTunnel.iPv6Synthesize(IPv4Addr);
   423                                      }
   424  
   425                                      @Override
   426                                      public long hasIPv6Route() {
   427                                          return PsiphonTunnel.hasIPv6Route(context, logger);
   428                                      }
   429                                  },
   430                                  new PsiphonProviderNoticeHandler() {
   431                                      @Override
   432                                      public void notice(String noticeJSON) {
   433  
   434                                          try {
   435                                              JSONObject notice = new JSONObject(noticeJSON);
   436  
   437                                              String noticeType = notice.getString("noticeType");
   438                                              if (noticeType == null) {
   439                                                  return;
   440                                              }
   441  
   442                                              JSONObject data = notice.getJSONObject("data");
   443                                              if (data == null) {
   444                                                  return;
   445                                              }
   446  
   447                                              String diagnosticMessage = noticeType + ": " + data.toString();
   448                                              callbackQueue.submit(new Runnable() {
   449                                                  @Override
   450                                                  public void run() {
   451                                                      logger.onDiagnosticMessage(diagnosticMessage);
   452                                                  }
   453                                              });
   454                                          } catch (java.lang.Exception e) {
   455                                              callbackQueue.submit(new Runnable() {
   456                                                  @Override
   457                                                  public void run() {
   458                                                      logger.onDiagnosticMessage("Error handling notice " + e.toString());
   459                                                  }
   460                                              });
   461                                          }
   462                                      }
   463                                  },
   464                                  false,   // Do not use IPv6 synthesizer for Android
   465                                  true     // Use hasIPv6Route on Android
   466                                  );
   467                      } catch (java.lang.Exception e) {
   468                          callbackQueue.submit(new Runnable() {
   469                              @Override
   470                              public void run() {
   471                                  feedbackHandler.sendFeedbackCompleted(new Exception("Error sending feedback", e));
   472                              }
   473                          });
   474                      }
   475                  }
   476              });
   477          }
   478  
   479          // Interrupt an in-progress feedback upload operation started with startSendFeedback(). This
   480          // call is asynchronous and returns a future which is fulfilled when the underlying stop
   481          // operation completes.
   482          public Future<Void> stopSendFeedback() {
   483              return workQueue.submit(new Runnable() {
   484                  @Override
   485                  public void run() {
   486                      Psi.stopSendFeedback();
   487                  }
   488              }, null);
   489          }
   490      }
   491  
   492      //----------------------------------------------------------------------------------------------
   493      // VPN Routing
   494      //----------------------------------------------------------------------------------------------
   495  
   496      private final static String VPN_INTERFACE_NETMASK = "255.255.255.0";
   497      private final static int VPN_INTERFACE_MTU = 1500;
   498      private final static int UDPGW_SERVER_PORT = 7300;
   499  
   500      // Note: Atomic variables used for getting/setting local proxy port, routing flag, and
   501      // tun fd, as these functions may be called via PsiphonProvider callbacks. Do not use
   502      // synchronized functions as stop() is synchronized and a deadlock is possible as callbacks
   503      // can be called while stop holds the lock.
   504  
   505      @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
   506      private boolean startVpn() throws Exception {
   507  
   508          mVpnMode.set(true);
   509          mPrivateAddress = selectPrivateAddress();
   510  
   511          Locale previousLocale = Locale.getDefault();
   512  
   513          final String errorMessage = "startVpn failed";
   514          try {
   515              // Workaround for https://code.google.com/p/android/issues/detail?id=61096
   516              Locale.setDefault(new Locale("en"));
   517  
   518              int mtu = VPN_INTERFACE_MTU;
   519              String dnsResolver = mPrivateAddress.mRouter;
   520  
   521              ParcelFileDescriptor tunFd =
   522                      ((VpnService.Builder) mHostService.newVpnServiceBuilder())
   523                              .setSession(mHostService.getAppName())
   524                              .setMtu(mtu)
   525                              .addAddress(mPrivateAddress.mIpAddress, mPrivateAddress.mPrefixLength)
   526                              .addRoute("0.0.0.0", 0)
   527                              .addRoute(mPrivateAddress.mSubnet, mPrivateAddress.mPrefixLength)
   528                              .addDnsServer(dnsResolver)
   529                              .establish();
   530              if (tunFd == null) {
   531                  // As per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29,
   532                  // this application is no longer prepared or was revoked.
   533                  return false;
   534              }
   535              mTunFd.set(tunFd);
   536              mRoutingThroughTunnel.set(false);
   537  
   538              mHostService.onDiagnosticMessage("VPN established");
   539  
   540          } catch(IllegalArgumentException e) {
   541              throw new Exception(errorMessage, e);
   542          } catch(IllegalStateException e) {
   543              throw new Exception(errorMessage, e);
   544          } catch(SecurityException e) {
   545              throw new Exception(errorMessage, e);
   546          } finally {
   547              // Restore the original locale.
   548              Locale.setDefault(previousLocale);
   549          }
   550  
   551          return true;
   552      }
   553  
   554      @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
   555      private ParcelFileDescriptor startDummyVpn(VpnService.Builder vpnServiceBuilder) throws Exception {
   556          PrivateAddress privateAddress = selectPrivateAddress();
   557  
   558          Locale previousLocale = Locale.getDefault();
   559  
   560          final String errorMessage = "startDummyVpn failed";
   561          final ParcelFileDescriptor tunFd;
   562          try {
   563              // Workaround for https://code.google.com/p/android/issues/detail?id=61096
   564              Locale.setDefault(new Locale("en"));
   565  
   566              int mtu = VPN_INTERFACE_MTU;
   567              String dnsResolver = privateAddress.mRouter;
   568  
   569              tunFd = vpnServiceBuilder
   570                              .setSession(mHostService.getAppName())
   571                              .setMtu(mtu)
   572                              .addAddress(privateAddress.mIpAddress, privateAddress.mPrefixLength)
   573                              .addRoute("0.0.0.0", 0)
   574                              .addRoute(privateAddress.mSubnet, privateAddress.mPrefixLength)
   575                              .addDnsServer(dnsResolver)
   576                              .establish();
   577          } catch(IllegalArgumentException e) {
   578              throw new Exception(errorMessage, e);
   579          } catch(IllegalStateException e) {
   580              throw new Exception(errorMessage, e);
   581          } catch(SecurityException e) {
   582              throw new Exception(errorMessage, e);
   583          } finally {
   584              // Restore the original locale.
   585              Locale.setDefault(previousLocale);
   586          }
   587  
   588          return tunFd;
   589      }
   590  
   591      private boolean isVpnMode() {
   592          return mVpnMode.get();
   593      }
   594  
   595      private void setLocalSocksProxyPort(int port) {
   596          mLocalSocksProxyPort.set(port);
   597      }
   598  
   599      private void stopVpn() {
   600          stopTun2Socks();
   601          ParcelFileDescriptor tunFd = mTunFd.getAndSet(null);
   602          if (tunFd != null) {
   603              try {
   604                  tunFd.close();
   605              } catch (IOException e) {
   606              }
   607          }
   608          mRoutingThroughTunnel.set(false);
   609      }
   610  
   611      //----------------------------------------------------------------------------------------------
   612      // PsiphonProvider (Core support) interface implementation
   613      //----------------------------------------------------------------------------------------------
   614  
   615      // The PsiphonProvider functions are called from Go, and must be public to be accessible
   616      // via the gobind mechanim. To avoid making internal implementation functions public,
   617      // PsiphonProviderShim is used as a wrapper.
   618  
   619      private class PsiphonProviderShim implements PsiphonProvider {
   620  
   621          private PsiphonTunnel mPsiphonTunnel;
   622  
   623          public PsiphonProviderShim(PsiphonTunnel psiphonTunnel) {
   624              mPsiphonTunnel = psiphonTunnel;
   625          }
   626  
   627          @Override
   628          public void notice(String noticeJSON) {
   629              mPsiphonTunnel.notice(noticeJSON);
   630          }
   631  
   632          @Override
   633          public String bindToDevice(long fileDescriptor) throws Exception {
   634              return mPsiphonTunnel.bindToDevice(fileDescriptor);
   635          }
   636  
   637          @Override
   638          public long hasNetworkConnectivity() {
   639              return mPsiphonTunnel.hasNetworkConnectivity();
   640          }
   641  
   642          @Override
   643          public String getDNSServersAsString() {
   644              return mPsiphonTunnel.getDNSServers(mHostService.getContext(), mHostService);
   645          }
   646  
   647          @Override
   648          public String iPv6Synthesize(String IPv4Addr) {
   649              return PsiphonTunnel.iPv6Synthesize(IPv4Addr);
   650          }
   651  
   652          @Override
   653          public long hasIPv6Route() {
   654              return PsiphonTunnel.hasIPv6Route(mHostService.getContext(), mHostService);
   655          }
   656  
   657          @Override
   658          public String getNetworkID() {
   659              return PsiphonTunnel.getNetworkID(mHostService.getContext());
   660          }
   661      }
   662  
   663      private void notice(String noticeJSON) {
   664          handlePsiphonNotice(noticeJSON);
   665      }
   666  
   667      @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
   668      private String bindToDevice(long fileDescriptor) throws Exception {
   669          if (!((VpnService)mHostService.getVpnService()).protect((int)fileDescriptor)) {
   670              throw new Exception("protect socket failed");
   671          }
   672          return "";
   673      }
   674  
   675      private long hasNetworkConnectivity() {
   676          boolean hasConnectivity = hasNetworkConnectivity(mHostService.getContext());
   677          boolean wasWaitingForNetworkConnectivity = mIsWaitingForNetworkConnectivity.getAndSet(!hasConnectivity);
   678          // HasNetworkConnectivity may be called many times, but only invoke
   679          // callbacks once per loss or resumption of connectivity, so, e.g.,
   680          // the HostService may log a single message.
   681          if (!hasConnectivity && !wasWaitingForNetworkConnectivity) {
   682              mHostService.onStartedWaitingForNetworkConnectivity();
   683          } else if (hasConnectivity && wasWaitingForNetworkConnectivity) {
   684              mHostService.onStoppedWaitingForNetworkConnectivity();
   685          }
   686          // TODO: change to bool return value once gobind supports that type
   687          return hasConnectivity ? 1 : 0;
   688      }
   689  
   690      private String getDNSServers(Context context, HostLogger logger) {
   691  
   692          // Use the DNS servers set by mNetworkMonitor,
   693          // mActiveNetworkDNSServers, when available. It's the most reliable
   694          // mechanism. Otherwise fallback to getActiveNetworkDNSServers.
   695          //
   696          // mActiveNetworkDNSServers is not available on API < 21
   697          // (LOLLIPOP). mActiveNetworkDNSServers may also be temporarily
   698          // unavailable if the last active network has been lost and no new
   699          // one has yet replaced it.
   700  
   701          String servers = mActiveNetworkDNSServers.get();
   702          if (servers != "") {
   703              return servers;
   704          }
   705  
   706          try {
   707              // Use the workaround, comma-delimited format required for gobind.
   708              servers = String.join(",", getActiveNetworkDNSServers(context, mVpnMode.get()));
   709          } catch (Exception e) {
   710              logger.onDiagnosticMessage("failed to get active network DNS resolver: " + e.getMessage());
   711              // Alternate DNS servers will be provided by psiphon-tunnel-core
   712              // config or tactics.
   713          }
   714          return servers;
   715      }
   716  
   717      private static String iPv6Synthesize(String IPv4Addr) {
   718          // Unused on Android.
   719          return IPv4Addr;
   720      }
   721  
   722      private static long hasIPv6Route(Context context, HostLogger logger) {
   723          boolean hasRoute = false;
   724          try {
   725              hasRoute = hasIPv6Route(context);
   726          } catch (Exception e) {
   727              logger.onDiagnosticMessage("failed to check IPv6 route: " + e.getMessage());
   728          }
   729          // TODO: change to bool return value once gobind supports that type
   730          return hasRoute ? 1 : 0;
   731      }
   732  
   733      private static String getNetworkID(Context context) {
   734  
   735          // TODO: getActiveNetworkInfo is deprecated in API 29; once
   736          // getActiveNetworkInfo is no longer available, use
   737          // mActiveNetworkType which is updated by mNetworkMonitor.
   738  
   739          // The network ID contains potential PII. In tunnel-core, the network ID
   740          // is used only locally in the client and not sent to the server.
   741          //
   742          // See network ID requirements here:
   743          // https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter
   744  
   745          String networkID = "UNKNOWN";
   746  
   747          ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
   748          NetworkInfo activeNetworkInfo = null;
   749          try {
   750              activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
   751  
   752          } catch (java.lang.Exception e) {
   753              // May get exceptions due to missing permissions like android.permission.ACCESS_NETWORK_STATE.
   754  
   755              // Apps using the Psiphon Library and lacking android.permission.ACCESS_NETWORK_STATE will
   756              // proceed and use tactics, but with "UNKNOWN" as the sole network ID.
   757          }
   758  
   759          if (activeNetworkInfo != null && activeNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
   760  
   761              networkID = "WIFI";
   762  
   763              try {
   764                  WifiManager wifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
   765                  WifiInfo wifiInfo = wifiManager.getConnectionInfo();
   766                  if (wifiInfo != null) {
   767                      String wifiNetworkID = wifiInfo.getBSSID();
   768                      if (wifiNetworkID.equals("02:00:00:00:00:00")) {
   769                          // "02:00:00:00:00:00" is reported when the app does not have the ACCESS_COARSE_LOCATION permission:
   770                          // https://developer.android.com/about/versions/marshmallow/android-6.0-changes#behavior-hardware-id
   771                          // The Psiphon client should allow the user to opt-in to this permission. If they decline, fail over
   772                          // to using the WiFi IP address.
   773                          wifiNetworkID = String.valueOf(wifiInfo.getIpAddress());
   774                      }
   775                      networkID += "-" + wifiNetworkID;
   776                  }
   777              } catch (java.lang.Exception e) {
   778                  // May get exceptions due to missing permissions like android.permission.ACCESS_WIFI_STATE.
   779                  // Fall through and use just "WIFI"
   780              }
   781  
   782          } else if (activeNetworkInfo != null && activeNetworkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
   783  
   784              networkID = "MOBILE";
   785  
   786              try {
   787                  TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
   788                  if (telephonyManager != null) {
   789                      networkID += "-" + telephonyManager.getNetworkOperator();
   790                  }
   791              } catch (java.lang.Exception e) {
   792                  // May get exceptions due to missing permissions.
   793                  // Fall through and use just "MOBILE"
   794              }
   795          }
   796  
   797          return networkID;
   798      }
   799  
   800      //----------------------------------------------------------------------------------------------
   801      // Psiphon Tunnel Core
   802      //----------------------------------------------------------------------------------------------
   803  
   804      private void startPsiphon(String embeddedServerEntries) throws Exception {
   805          stopPsiphon();
   806          mIsWaitingForNetworkConnectivity.set(false);
   807          mHostService.onDiagnosticMessage("starting Psiphon library");
   808          try {
   809              Psi.start(
   810                      loadPsiphonConfig(mHostService.getContext()),
   811                      embeddedServerEntries,
   812                      "",
   813                      new PsiphonProviderShim(this),
   814                      isVpnMode(),
   815                      false,   // Do not use IPv6 synthesizer for Android
   816                      true     // Use hasIPv6Route on Android
   817                      );
   818          } catch (java.lang.Exception e) {
   819              throw new Exception("failed to start Psiphon library", e);
   820          }
   821  
   822          mNetworkMonitor.start(mHostService.getContext());
   823          mHostService.onDiagnosticMessage("Psiphon library started");
   824      }
   825  
   826      private void stopPsiphon() {
   827          mHostService.onDiagnosticMessage("stopping Psiphon library");
   828          mNetworkMonitor.stop(mHostService.getContext());
   829          Psi.stop();
   830          mHostService.onDiagnosticMessage("Psiphon library stopped");
   831      }
   832  
   833      private String loadPsiphonConfig(Context context)
   834              throws IOException, JSONException, Exception {
   835  
   836          return buildPsiphonConfig(context, mHostService, mHostService.getPsiphonConfig(),
   837                  mClientPlatformPrefix.get(), mClientPlatformSuffix.get(), isVpnMode(),
   838                  mLocalSocksProxyPort.get());
   839      }
   840  
   841      private static String buildPsiphonConfig(Context context, HostLogger logger, String psiphonConfig,
   842                                               String clientPlatformPrefix, String clientPlatformSuffix,
   843                                               boolean isVpnMode, Integer localSocksProxyPort)
   844              throws IOException, JSONException, Exception {
   845  
   846          // Load settings from the raw resource JSON config file and
   847          // update as necessary. Then write JSON to disk for the Go client.
   848          JSONObject json = new JSONObject(psiphonConfig);
   849  
   850          // On Android, this directory must be set to the app private storage area.
   851          // The Psiphon library won't be able to use its current working directory
   852          // and the standard temporary directories do not exist.
   853          if (!json.has("DataRootDirectory")) {
   854              File dataRootDirectory = defaultDataRootDirectory(context);
   855              if (!dataRootDirectory.exists()) {
   856                  boolean created = dataRootDirectory.mkdir();
   857                  if (!created) {
   858                      throw new Exception("failed to create data root directory: " + dataRootDirectory.getPath());
   859                  }
   860              }
   861              json.put("DataRootDirectory", defaultDataRootDirectory(context));
   862          }
   863  
   864          // Migrate datastore files from legacy directory.
   865          if (!json.has("DataStoreDirectory")) {
   866              json.put("MigrateDataStoreDirectory", context.getFilesDir());
   867          }
   868  
   869          // Migrate remote server list downloads from legacy location.
   870          if (!json.has("RemoteServerListDownloadFilename")) {
   871              File remoteServerListDownload = new File(context.getFilesDir(), "remote_server_list");
   872              json.put("MigrateRemoteServerListDownloadFilename", remoteServerListDownload.getAbsolutePath());
   873          }
   874  
   875          // Migrate obfuscated server list download files from legacy directory.
   876          File oslDownloadDir = new File(context.getFilesDir(), "osl");
   877          json.put("MigrateObfuscatedServerListDownloadDirectory", oslDownloadDir.getAbsolutePath());
   878  
   879          // Continue to run indefinitely until connected
   880          if (!json.has("EstablishTunnelTimeoutSeconds")) {
   881              json.put("EstablishTunnelTimeoutSeconds", 0);
   882          }
   883  
   884          json.put("EmitBytesTransferred", true);
   885  
   886          if (localSocksProxyPort != 0 && (!json.has("LocalSocksProxyPort") || json.getInt("LocalSocksProxyPort") == 0)) {
   887              // When mLocalSocksProxyPort is set, tun2socks is already configured
   888              // to use that port value. So we force use of the same port.
   889              // A side-effect of this is that changing the SOCKS port preference
   890              // has no effect with restartPsiphon(), a full stop() is necessary.
   891              json.put("LocalSocksProxyPort", localSocksProxyPort);
   892          }
   893  
   894          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
   895              try {
   896                  json.put(
   897                          "TrustedCACertificatesFilename",
   898                          setupTrustedCertificates(context, logger));
   899              } catch (Exception e) {
   900                  logger.onDiagnosticMessage(e.getMessage());
   901              }
   902          }
   903  
   904          json.put("DeviceRegion", getDeviceRegion(context));
   905  
   906          StringBuilder clientPlatform = new StringBuilder();
   907  
   908          if (clientPlatformPrefix.length() > 0) {
   909              clientPlatform.append(clientPlatformPrefix);
   910          }
   911  
   912          clientPlatform.append("Android_");
   913          clientPlatform.append(Build.VERSION.RELEASE);
   914          clientPlatform.append("_");
   915          clientPlatform.append(context.getPackageName());
   916  
   917          if (clientPlatformSuffix.length() > 0) {
   918              clientPlatform.append(clientPlatformSuffix);
   919          }
   920  
   921          json.put("ClientPlatform", clientPlatform.toString().replaceAll("[^\\w\\-\\.]", "_"));
   922  
   923          return json.toString();
   924      }
   925  
   926      private void handlePsiphonNotice(String noticeJSON) {
   927          try {
   928              // All notices are sent on as diagnostic messages
   929              // except those that may contain private user data.
   930              boolean diagnostic = true;
   931  
   932              JSONObject notice = new JSONObject(noticeJSON);
   933              String noticeType = notice.getString("noticeType");
   934  
   935              if (noticeType.equals("Tunnels")) {
   936                  int count = notice.getJSONObject("data").getInt("count");
   937                  if (count == 0) {
   938                      mHostService.onConnecting();
   939                  } else if (count == 1) {
   940                      if (isVpnMode() && mShouldRouteThroughTunnelAutomatically) {
   941                          routeThroughTunnel();
   942                      }
   943                      mHostService.onConnected();
   944                  }
   945                  // count > 1 is an additional multi-tunnel establishment, and not reported.
   946  
   947              } else if (noticeType.equals("AvailableEgressRegions")) {
   948                  JSONArray egressRegions = notice.getJSONObject("data").getJSONArray("regions");
   949                  ArrayList<String> regions = new ArrayList<String>();
   950                  for (int i=0; i<egressRegions.length(); i++) {
   951                      regions.add(egressRegions.getString(i));
   952                  }
   953                  mHostService.onAvailableEgressRegions(regions);
   954              } else if (noticeType.equals("SocksProxyPortInUse")) {
   955                  mHostService.onSocksProxyPortInUse(notice.getJSONObject("data").getInt("port"));
   956              } else if (noticeType.equals("HttpProxyPortInUse")) {
   957                  mHostService.onHttpProxyPortInUse(notice.getJSONObject("data").getInt("port"));
   958              } else if (noticeType.equals("ListeningSocksProxyPort")) {
   959                  int port = notice.getJSONObject("data").getInt("port");
   960                  setLocalSocksProxyPort(port);
   961                  mHostService.onListeningSocksProxyPort(port);
   962              } else if (noticeType.equals("ListeningHttpProxyPort")) {
   963                  int port = notice.getJSONObject("data").getInt("port");
   964                  mHostService.onListeningHttpProxyPort(port);
   965              } else if (noticeType.equals("UpstreamProxyError")) {
   966                  diagnostic = false;
   967                  mHostService.onUpstreamProxyError(notice.getJSONObject("data").getString("message"));
   968              } else if (noticeType.equals("ClientUpgradeDownloaded")) {
   969                  mHostService.onClientUpgradeDownloaded(notice.getJSONObject("data").getString("filename"));
   970              } else if (noticeType.equals("ClientIsLatestVersion")) {
   971                  mHostService.onClientIsLatestVersion();
   972              } else if (noticeType.equals("Homepage")) {
   973                  mHostService.onHomepage(notice.getJSONObject("data").getString("url"));
   974              } else if (noticeType.equals("ClientRegion")) {
   975                  mHostService.onClientRegion(notice.getJSONObject("data").getString("region"));
   976              } else if (noticeType.equals("ClientAddress")) {
   977                  diagnostic = false;
   978                  mHostService.onClientAddress(notice.getJSONObject("data").getString("address"));
   979              } else if (noticeType.equals("SplitTunnelRegions")) {
   980                  JSONArray splitTunnelRegions = notice.getJSONObject("data").getJSONArray("regions");
   981                  ArrayList<String> regions = new ArrayList<String>();
   982                  for (int i=0; i<splitTunnelRegions.length(); i++) {
   983                      regions.add(splitTunnelRegions.getString(i));
   984                  }
   985                  mHostService.onSplitTunnelRegions(regions);
   986              } else if (noticeType.equals("Untunneled")) {
   987                  diagnostic = false;
   988                  mHostService.onUntunneledAddress(notice.getJSONObject("data").getString("address"));
   989              } else if (noticeType.equals("BytesTransferred")) {
   990                  diagnostic = false;
   991                  JSONObject data = notice.getJSONObject("data");
   992                  mHostService.onBytesTransferred(data.getLong("sent"), data.getLong("received"));
   993              } else if (noticeType.equals("ActiveAuthorizationIDs")) {
   994                  JSONArray activeAuthorizationIDs = notice.getJSONObject("data").getJSONArray("IDs");
   995                  ArrayList<String> authorizations = new ArrayList<String>();
   996                  for (int i=0; i<activeAuthorizationIDs.length(); i++) {
   997                      authorizations.add(activeAuthorizationIDs.getString(i));
   998                  }
   999                  mHostService.onActiveAuthorizationIDs(authorizations);
  1000              } else if (noticeType.equals("TrafficRateLimits")) {
  1001                  JSONObject data = notice.getJSONObject("data");
  1002                  mHostService.onTrafficRateLimits(
  1003                      data.getLong("upstreamBytesPerSecond"), data.getLong("downstreamBytesPerSecond"));
  1004              } else if (noticeType.equals("Exiting")) {
  1005                  mHostService.onExiting();
  1006              } else if (noticeType.equals("ActiveTunnel")) {
  1007                  if (isVpnMode()) {
  1008                      if (notice.getJSONObject("data").getBoolean("isTCS")) {
  1009                        disableUdpGwKeepalive();
  1010                      } else {
  1011                        enableUdpGwKeepalive();
  1012                      }
  1013                  }
  1014              } else if (noticeType.equals("ApplicationParameter")) {
  1015                  mHostService.onApplicationParameter(
  1016                      notice.getJSONObject("data").getString("key"),
  1017                      notice.getJSONObject("data").get("value"));
  1018              } else if (noticeType.equals("ServerAlert")) {
  1019                  JSONArray actionURLs = notice.getJSONObject("data").getJSONArray("actionURLs");
  1020                  ArrayList<String> actionURLsList = new ArrayList<String>();
  1021                  for (int i=0; i<actionURLs.length(); i++) {
  1022                      actionURLsList.add(actionURLs.getString(i));
  1023                  }
  1024                  mHostService.onServerAlert(
  1025                      notice.getJSONObject("data").getString("reason"),
  1026                      notice.getJSONObject("data").getString("subject"),
  1027                      actionURLsList);
  1028              }
  1029  
  1030              if (diagnostic) {
  1031                  String diagnosticMessage = noticeType + ": " + notice.getJSONObject("data").toString();
  1032                  mHostService.onDiagnosticMessage(diagnosticMessage);
  1033              }
  1034  
  1035          } catch (JSONException e) {
  1036              // Ignore notice
  1037          }
  1038      }
  1039  
  1040      private static String setupTrustedCertificates(Context context, HostLogger logger) throws Exception {
  1041  
  1042          // Copy the Android system CA store to a local, private cert bundle file.
  1043          //
  1044          // This results in a file that can be passed to SSL_CTX_load_verify_locations
  1045          // for use with OpenSSL modes in tunnel-core.
  1046          // https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_load_verify_locations.html
  1047          //
  1048          // TODO: to use the path mode of load_verify_locations would require emulating
  1049          // the filename scheme used by c_rehash:
  1050          // https://www.openssl.org/docs/manmaster/apps/c_rehash.html
  1051          // http://stackoverflow.com/questions/19237167/the-new-subject-hash-openssl-algorithm-differs
  1052  
  1053          File directory = context.getDir("PsiphonCAStore", Context.MODE_PRIVATE);
  1054  
  1055          final String errorMessage = "copy AndroidCAStore failed";
  1056          try {
  1057  
  1058              File file = new File(directory, "certs.dat");
  1059  
  1060              // Pave a fresh copy on every run, which ensures we're not using old certs.
  1061              // Note: assumes KeyStore doesn't return revoked certs.
  1062              //
  1063              // TODO: this takes under 1 second, but should we avoid repaving every time?
  1064              file.delete();
  1065  
  1066              PrintStream output = null;
  1067              try {
  1068                  output = new PrintStream(new FileOutputStream(file));
  1069  
  1070                  KeyStore keyStore;
  1071                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
  1072                      keyStore = KeyStore.getInstance("AndroidCAStore");
  1073                      keyStore.load(null, null);
  1074                  } else {
  1075                      keyStore = KeyStore.getInstance("BKS");
  1076                      FileInputStream inputStream = new FileInputStream("/etc/security/cacerts.bks");
  1077                      try {
  1078                          keyStore.load(inputStream, "changeit".toCharArray());
  1079                      } finally {
  1080                          if (inputStream != null) {
  1081                              inputStream.close();
  1082                          }
  1083                      }
  1084                  }
  1085  
  1086                  Enumeration<String> aliases = keyStore.aliases();
  1087                  while (aliases.hasMoreElements()) {
  1088                      String alias = aliases.nextElement();
  1089                      X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);
  1090  
  1091                      output.println("-----BEGIN CERTIFICATE-----");
  1092                      String pemCert = new String(Base64.encode(cert.getEncoded(), Base64.NO_WRAP), "UTF-8");
  1093                      // OpenSSL appears to reject the default linebreaking done by Base64.encode,
  1094                      // so we manually linebreak every 64 characters
  1095                      for (int i = 0; i < pemCert.length() ; i+= 64) {
  1096                          output.println(pemCert.substring(i, Math.min(i + 64, pemCert.length())));
  1097                      }
  1098                      output.println("-----END CERTIFICATE-----");
  1099                  }
  1100  
  1101                  logger.onDiagnosticMessage("prepared PsiphonCAStore");
  1102  
  1103                  return file.getAbsolutePath();
  1104  
  1105              } finally {
  1106                  if (output != null) {
  1107                      output.close();
  1108                  }
  1109              }
  1110  
  1111          } catch (KeyStoreException e) {
  1112              throw new Exception(errorMessage, e);
  1113          } catch (NoSuchAlgorithmException e) {
  1114              throw new Exception(errorMessage, e);
  1115          } catch (CertificateException e) {
  1116              throw new Exception(errorMessage, e);
  1117          } catch (IOException e) {
  1118              throw new Exception(errorMessage, e);
  1119          }
  1120      }
  1121  
  1122      private static String getDeviceRegion(Context context) {
  1123          String region = "";
  1124          TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
  1125          if (telephonyManager != null) {
  1126              region = telephonyManager.getSimCountryIso();
  1127              if (region == null) {
  1128                  region = "";
  1129              }
  1130              if (region.length() == 0 && telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) {
  1131                  region = telephonyManager.getNetworkCountryIso();
  1132                  if (region == null) {
  1133                      region = "";
  1134                  }
  1135              }
  1136          }
  1137          if (region.length() == 0) {
  1138              Locale defaultLocale = Locale.getDefault();
  1139              if (defaultLocale != null) {
  1140                  region = defaultLocale.getCountry();
  1141              }
  1142          }
  1143          return region.toUpperCase(Locale.US);
  1144      }
  1145  
  1146      //----------------------------------------------------------------------------------------------
  1147      // Tun2Socks
  1148      //----------------------------------------------------------------------------------------------
  1149  
  1150      @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
  1151      private void startTun2Socks(
  1152              final ParcelFileDescriptor vpnInterfaceFileDescriptor,
  1153              final int vpnInterfaceMTU,
  1154              final String vpnIpAddress,
  1155              final String vpnNetMask,
  1156              final String socksServerAddress,
  1157              final String udpgwServerAddress,
  1158              final boolean udpgwTransparentDNS) {
  1159          if (mTun2SocksThread != null) {
  1160              return;
  1161          }
  1162          mTun2SocksThread = new Thread(new Runnable() {
  1163              @Override
  1164              public void run() {
  1165                  runTun2Socks(
  1166                          vpnInterfaceFileDescriptor.detachFd(),
  1167                          vpnInterfaceMTU,
  1168                          vpnIpAddress,
  1169                          vpnNetMask,
  1170                          socksServerAddress,
  1171                          udpgwServerAddress,
  1172                          udpgwTransparentDNS ? 1 : 0);
  1173              }
  1174          });
  1175          mTun2SocksThread.start();
  1176          mHostService.onDiagnosticMessage("tun2socks started");
  1177      }
  1178  
  1179      private void stopTun2Socks() {
  1180          if (mTun2SocksThread != null) {
  1181              try {
  1182                  terminateTun2Socks();
  1183                  mTun2SocksThread.join();
  1184              } catch (InterruptedException e) {
  1185                  Thread.currentThread().interrupt();
  1186              }
  1187              mTun2SocksThread = null;
  1188              mHostService.onDiagnosticMessage("tun2socks stopped");
  1189          }
  1190      }
  1191  
  1192      public static void logTun2Socks(String level, String channel, String msg) {
  1193          String logMsg = "tun2socks: " + level + "(" + channel + "): " + msg;
  1194          mPsiphonTunnel.mHostService.onDiagnosticMessage(logMsg);
  1195      }
  1196  
  1197      private native static int runTun2Socks(
  1198              int vpnInterfaceFileDescriptor,
  1199              int vpnInterfaceMTU,
  1200              String vpnIpAddress,
  1201              String vpnNetMask,
  1202              String socksServerAddress,
  1203              String udpgwServerAddress,
  1204              int udpgwTransparentDNS);
  1205  
  1206      private native static int terminateTun2Socks();
  1207  
  1208      private native static int enableUdpGwKeepalive();
  1209      private native static int disableUdpGwKeepalive();
  1210  
  1211      //----------------------------------------------------------------------------------------------
  1212      // Implementation: Network Utils
  1213      //----------------------------------------------------------------------------------------------
  1214  
  1215      private static boolean hasNetworkConnectivity(Context context) {
  1216          ConnectivityManager connectivityManager =
  1217                  (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1218          if (connectivityManager == null) {
  1219              return false;
  1220          }
  1221          NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
  1222          return networkInfo != null && networkInfo.isConnected();
  1223      }
  1224  
  1225      private static class PrivateAddress {
  1226          final public String mIpAddress;
  1227          final public String mSubnet;
  1228          final public int mPrefixLength;
  1229          final public String mRouter;
  1230          public PrivateAddress(String ipAddress, String subnet, int prefixLength, String router) {
  1231              mIpAddress = ipAddress;
  1232              mSubnet = subnet;
  1233              mPrefixLength = prefixLength;
  1234              mRouter = router;
  1235          }
  1236      }
  1237  
  1238      private static PrivateAddress selectPrivateAddress() throws Exception {
  1239          // Select one of 10.0.0.1, 172.16.0.1, or 192.168.0.1 depending on
  1240          // which private address range isn't in use.
  1241  
  1242          Map<String, PrivateAddress> candidates = new HashMap<String, PrivateAddress>();
  1243          candidates.put( "10", new PrivateAddress("10.0.0.1",    "10.0.0.0",     8, "10.0.0.2"));
  1244          candidates.put("172", new PrivateAddress("172.16.0.1",  "172.16.0.0",  12, "172.16.0.2"));
  1245          candidates.put("192", new PrivateAddress("192.168.0.1", "192.168.0.0", 16, "192.168.0.2"));
  1246          candidates.put("169", new PrivateAddress("169.254.1.1", "169.254.1.0", 24, "169.254.1.2"));
  1247  
  1248          Enumeration<NetworkInterface> netInterfaces;
  1249          try {
  1250              netInterfaces = NetworkInterface.getNetworkInterfaces();
  1251          } catch (SocketException e) {
  1252              throw new Exception("selectPrivateAddress failed", e);
  1253          }
  1254  
  1255          if (netInterfaces == null) {
  1256              throw new Exception("no network interfaces found");
  1257          }
  1258  
  1259          for (NetworkInterface netInterface : Collections.list(netInterfaces)) {
  1260              for (InetAddress inetAddress : Collections.list(netInterface.getInetAddresses())) {
  1261                  if (inetAddress instanceof Inet4Address) {
  1262                      String ipAddress = inetAddress.getHostAddress();
  1263                      if (ipAddress.startsWith("10.")) {
  1264                          candidates.remove("10");
  1265                      }
  1266                      else if (
  1267                              ipAddress.length() >= 6 &&
  1268                                      ipAddress.substring(0, 6).compareTo("172.16") >= 0 &&
  1269                                      ipAddress.substring(0, 6).compareTo("172.31") <= 0) {
  1270                          candidates.remove("172");
  1271                      }
  1272                      else if (ipAddress.startsWith("192.168")) {
  1273                          candidates.remove("192");
  1274                      }
  1275                  }
  1276              }
  1277          }
  1278  
  1279          if (candidates.size() > 0) {
  1280              return candidates.values().iterator().next();
  1281          }
  1282  
  1283          throw new Exception("no private address available");
  1284      }
  1285  
  1286      private static Collection<String> getActiveNetworkDNSServers(Context context, boolean isVpnMode)
  1287              throws Exception {
  1288  
  1289          ArrayList<String> servers = new ArrayList<String>();
  1290          for (InetAddress serverAddress : getActiveNetworkDNSServerAddresses(context, isVpnMode)) {
  1291              String server = serverAddress.toString();
  1292              // strip the leading slash e.g., "/192.168.1.1"
  1293              if (server.startsWith("/")) {
  1294                  server = server.substring(1);
  1295              }
  1296              servers.add(server);
  1297          }
  1298  
  1299          if (servers.isEmpty()) {
  1300              throw new Exception("no active network DNS resolver");
  1301          }
  1302  
  1303          return servers;
  1304      }
  1305  
  1306      private static Collection<InetAddress> getActiveNetworkDNSServerAddresses(Context context, boolean isVpnMode)
  1307              throws Exception {
  1308  
  1309          final String errorMessage = "getActiveNetworkDNSServerAddresses failed";
  1310          ArrayList<InetAddress> dnsAddresses = new ArrayList<InetAddress>();
  1311  
  1312          ConnectivityManager connectivityManager =
  1313                  (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1314          if (connectivityManager == null) {
  1315              throw new Exception(errorMessage, new Throwable("couldn't get ConnectivityManager system service"));
  1316          }
  1317  
  1318          try {
  1319  
  1320              // Hidden API:
  1321              //
  1322              // - Only available in Android 4.0+
  1323              // - No guarantee will be available beyond 4.2, or on all vendor
  1324              //   devices
  1325              // - Field reports indicate this is no longer working on some --
  1326              //   but not all -- Android 10+ devices
  1327  
  1328              Class<?> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
  1329              Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{});
  1330              Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager);
  1331              if (linkProperties != null) {
  1332                  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
  1333                      Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{});
  1334                      Collection<?> dnses = (Collection<?>)getDnsesMethod.invoke(linkProperties);
  1335                      for (Object dns : dnses) {
  1336                          dnsAddresses.add((InetAddress)dns);
  1337                      }
  1338                  } else {
  1339                      // LinkProperties is public in API 21 (and the DNS function signature has changed)
  1340                      for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) {
  1341                          dnsAddresses.add(dns);
  1342                      }
  1343                  }
  1344              }
  1345          } catch (ClassNotFoundException e) {
  1346          } catch (NoSuchMethodException e) {
  1347          } catch (IllegalArgumentException e) {
  1348          } catch (IllegalAccessException e) {
  1349          } catch (InvocationTargetException e) {
  1350          } catch (NullPointerException e) {
  1351          }
  1352  
  1353          if (!dnsAddresses.isEmpty()) {
  1354              return dnsAddresses;
  1355          }
  1356  
  1357          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  1358  
  1359              // This case is attempted only when the hidden API fails:
  1360              //
  1361              // - Testing shows the hidden API still works more reliably on
  1362              //   some Android 11+ devices
  1363              // - Testing indicates that the NetworkRequest can sometimes
  1364              //   select the wrong network
  1365              //   - e.g., mobile instead of WiFi, and return the wrong DNS
  1366              //     servers
  1367              //   - there's currently no way to filter for the "currently
  1368              //     active default data network" returned by, e.g., the
  1369              //     deprecated getActiveNetworkInfo
  1370              //   - we cannot add the NET_CAPABILITY_FOREGROUND capability to
  1371              //     the NetworkRequest at this time due to target SDK
  1372              //     constraints
  1373  
  1374              NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder()
  1375                  .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
  1376  
  1377              if (isVpnMode) {
  1378                  // In VPN mode, we want the DNS servers for the underlying physical network.
  1379                  networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
  1380              }
  1381  
  1382              NetworkRequest networkRequest = networkRequestBuilder.build();
  1383  
  1384              final CountDownLatch countDownLatch = new CountDownLatch(1);
  1385              try {
  1386                  ConnectivityManager.NetworkCallback networkCallback =
  1387                          new ConnectivityManager.NetworkCallback() {
  1388                              @Override
  1389                              public void onLinkPropertiesChanged(Network network,
  1390                                                                  LinkProperties linkProperties) {
  1391                                  dnsAddresses.addAll(linkProperties.getDnsServers());
  1392                                  countDownLatch.countDown();
  1393                              }
  1394                          };
  1395  
  1396                  connectivityManager.registerNetworkCallback(networkRequest, networkCallback);
  1397                  countDownLatch.await(1, TimeUnit.SECONDS);
  1398                  connectivityManager.unregisterNetworkCallback(networkCallback);
  1399              } catch (RuntimeException ignored) {
  1400                  // Failed to register network callback
  1401              } catch (InterruptedException e) {
  1402                  Thread.currentThread().interrupt();
  1403              }
  1404          }
  1405  
  1406          return dnsAddresses;
  1407      }
  1408  
  1409      private static boolean hasIPv6Route(Context context) throws Exception {
  1410  
  1411              try {
  1412                  // This logic mirrors the logic in
  1413                  // psiphon/common/resolver.hasRoutableIPv6Interface. That
  1414                  // function currently doesn't work on Android due to Go's
  1415                  // net.InterfaceAddrs failing on Android SDK 30+ (see Go issue
  1416                  // 40569). hasIPv6Route provides the same functionality via a
  1417                  // callback into Java code.
  1418  
  1419                  for (NetworkInterface netInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
  1420                      if (netInterface.isUp() &&
  1421                          !netInterface.isLoopback() &&
  1422                          !netInterface.isPointToPoint()) {
  1423                          for (InetAddress address : Collections.list(netInterface.getInetAddresses())) {
  1424  
  1425                              // Per https://developer.android.com/reference/java/net/Inet6Address#textual-representation-of-ip-addresses,
  1426                              // "Java will never return an IPv4-mapped address.
  1427                              //  These classes can take an IPv4-mapped address as
  1428                              //  input, both in byte array and text
  1429                              //  representation. However, it will be converted
  1430                              //  into an IPv4 address." As such, when the type of
  1431                              //  the IP address is Inet6Address, this should be
  1432                              //  an actual IPv6 address.
  1433  
  1434                              if (address instanceof Inet6Address &&
  1435                                  !address.isLinkLocalAddress() &&
  1436                                  !address.isSiteLocalAddress() &&
  1437                                  !address.isMulticastAddress ()) {
  1438                                  return true;
  1439                              }
  1440                          }
  1441                      }
  1442                  }
  1443                  } catch (SocketException e) {
  1444                  throw new Exception("hasIPv6Route failed", e);
  1445              }
  1446  
  1447              return false;
  1448      }
  1449  
  1450      //----------------------------------------------------------------------------------------------
  1451      // Exception
  1452      //----------------------------------------------------------------------------------------------
  1453  
  1454      public static class Exception extends java.lang.Exception {
  1455          private static final long serialVersionUID = 1L;
  1456          public Exception(String message) {
  1457              super(message);
  1458          }
  1459          public Exception(String message, Throwable cause) {
  1460              super(message + ": " + cause.getMessage());
  1461          }
  1462      }
  1463  
  1464      //----------------------------------------------------------------------------------------------
  1465      // Network connectivity monitor
  1466      //----------------------------------------------------------------------------------------------
  1467  
  1468      private static class NetworkMonitor {
  1469          private final NetworkChangeListener listener;
  1470          private ConnectivityManager.NetworkCallback networkCallback;
  1471          private AtomicReference<String> activeNetworkType;
  1472          private AtomicReference<String> activeNetworkDNSServers;
  1473          private HostLogger logger;
  1474  
  1475          public NetworkMonitor(
  1476              NetworkChangeListener listener,
  1477              AtomicReference<String> activeNetworkType,
  1478              AtomicReference<String> activeNetworkDNSServers,
  1479              HostLogger logger) {
  1480  
  1481              this.listener = listener;
  1482              this.activeNetworkType = activeNetworkType;
  1483              this.activeNetworkDNSServers = activeNetworkDNSServers;
  1484              this.logger = logger;
  1485          }
  1486  
  1487          private void start(Context context) {
  1488              // Need API 21(LOLLIPOP)+ for ConnectivityManager.NetworkCallback
  1489              if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
  1490                  return;
  1491              }
  1492              ConnectivityManager connectivityManager =
  1493                      (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1494              if (connectivityManager == null) {
  1495                  return;
  1496              }
  1497              networkCallback = new ConnectivityManager.NetworkCallback() {
  1498                  private boolean isInitialState = true;
  1499                  private Network currentActiveNetwork;
  1500  
  1501                  private void consumeActiveNetwork(Network network) {
  1502                      if (isInitialState) {
  1503                          isInitialState = false;
  1504                          setCurrentActiveNetworkAndProperties(network);
  1505                          return;
  1506                      }
  1507  
  1508                      if (!network.equals(currentActiveNetwork)) {
  1509                          setCurrentActiveNetworkAndProperties(network);
  1510                          if (listener != null) {
  1511                              listener.onChanged();
  1512                          }
  1513                      }
  1514                  }
  1515  
  1516                  private void consumeLostNetwork(Network network) {
  1517                      if (network.equals(currentActiveNetwork)) {
  1518                          setCurrentActiveNetworkAndProperties(null);
  1519                          if (listener != null) {
  1520                              listener.onChanged();
  1521                          }
  1522                      }
  1523                  }
  1524  
  1525                  private void setCurrentActiveNetworkAndProperties(Network network) {
  1526  
  1527                      currentActiveNetwork = network;
  1528  
  1529                      if (network == null) {
  1530  
  1531                          activeNetworkType.set("NONE");
  1532                          activeNetworkDNSServers.set("");
  1533                          logger.onDiagnosticMessage("NetworkMonitor: clear current active network");
  1534  
  1535                      } else {
  1536  
  1537                          String networkType = "UNKNOWN";
  1538                          try {
  1539                              // Limitation: a network may have both CELLULAR
  1540                              // and WIFI transports, or different network
  1541                              // transport types entirely. This logic currently
  1542                              // mimics the type determination logic in
  1543                              // getNetworkID.
  1544                              NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
  1545                              if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
  1546                                  networkType = "MOBILE";
  1547                              } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
  1548                                  networkType = "WIFI";
  1549                              }
  1550                          } catch (java.lang.Exception e) {
  1551                          }
  1552                          activeNetworkType.set(networkType);
  1553  
  1554                          ArrayList<String> servers = new ArrayList<String>();
  1555                          try {
  1556                              LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
  1557                              List<InetAddress> serverAddresses = linkProperties.getDnsServers();
  1558                              for (InetAddress serverAddress : serverAddresses) {
  1559                                  String server = serverAddress.toString();
  1560                                  if (server.startsWith("/")) {
  1561                                      server = server.substring(1);
  1562                                  }
  1563                                  servers.add(server);
  1564                              }
  1565                          } catch (java.lang.Exception e) {
  1566                          }
  1567                          // Use the workaround, comma-delimited format required for gobind.
  1568                          activeNetworkDNSServers.set(String.join(",", servers));
  1569  
  1570                          String message = "NetworkMonitor: set current active network " + networkType;
  1571                          if (!servers.isEmpty()) {
  1572                              // The DNS server address is potential PII and not logged.
  1573                              message += " with DNS";
  1574                          }
  1575                          logger.onDiagnosticMessage(message);
  1576                      }
  1577                  }
  1578  
  1579                  @Override
  1580                  public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
  1581                      super.onCapabilitiesChanged(network, capabilities);
  1582  
  1583                      // Need API 23(M)+ for NET_CAPABILITY_VALIDATED
  1584                      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
  1585                          return;
  1586                      }
  1587  
  1588                      // https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED
  1589                      // Indicates that connectivity on this network was successfully validated.
  1590                      // For example, for a network with NET_CAPABILITY_INTERNET, it means that Internet connectivity was
  1591                      // successfully detected.
  1592                      if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
  1593                          consumeActiveNetwork(network);
  1594                      }
  1595                  }
  1596  
  1597                  @Override
  1598                  public void onAvailable(Network network) {
  1599                      super.onAvailable(network);
  1600  
  1601                      // Skip on API 26(O)+ because onAvailable is guaranteed to be followed by
  1602                      // onCapabilitiesChanged
  1603                      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  1604                          return;
  1605                      }
  1606                      consumeActiveNetwork(network);
  1607                  }
  1608  
  1609                  @Override
  1610                  public void onLost(Network network) {
  1611                      super.onLost(network);
  1612                      consumeLostNetwork(network);
  1613                  }
  1614              };
  1615  
  1616              try {
  1617                  // When searching for a network to satisfy a request, all capabilities requested must be satisfied.
  1618                  NetworkRequest.Builder builder = new NetworkRequest.Builder()
  1619                          // Indicates that this network should be able to reach the internet.
  1620                          .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
  1621  
  1622                  if (mPsiphonTunnel.mVpnMode.get()) {
  1623                      // If we are in the VPN mode then ensure we monitor only the VPN's underlying
  1624                      // active networks and not self.
  1625                      builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
  1626                  } else {
  1627                      // If we are NOT in the VPN mode then monitor default active networks with the
  1628                      // Internet capability, including VPN, to ensure we won't trigger a reconnect in
  1629                      // case the VPN is up while the system switches the underlying network.
  1630                      builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
  1631                  }
  1632  
  1633                  NetworkRequest networkRequest = builder.build();
  1634                  connectivityManager.requestNetwork(networkRequest, networkCallback);
  1635              } catch (RuntimeException ignored) {
  1636                  // Could be a security exception or any other runtime exception on customized firmwares.
  1637                  networkCallback = null;
  1638              }
  1639          }
  1640  
  1641          private void stop(Context context) {
  1642              if (networkCallback == null) {
  1643                  return;
  1644              }
  1645              // Need API 21(LOLLIPOP)+ for ConnectivityManager.NetworkCallback
  1646              if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
  1647                  return;
  1648              }
  1649              ConnectivityManager connectivityManager =
  1650                      (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1651              if (connectivityManager == null) {
  1652                  return;
  1653              }
  1654              // Note: ConnectivityManager.unregisterNetworkCallback() may throw
  1655              // "java.lang.IllegalArgumentException: NetworkCallback was not registered".
  1656              // This scenario should be handled in the start() above but we'll add a try/catch
  1657              // anyway to match the start's call to ConnectivityManager.registerNetworkCallback()
  1658              try {
  1659                  connectivityManager.unregisterNetworkCallback(networkCallback);
  1660              } catch (RuntimeException ignored) {
  1661              }
  1662              networkCallback = null;
  1663          }
  1664  
  1665          public interface NetworkChangeListener {
  1666              void onChanged();
  1667          }
  1668      }
  1669  }