github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/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 onApplicationParameters(Object parameters) {} 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("ApplicationParameters")) { 1015 mHostService.onApplicationParameters( 1016 notice.getJSONObject("data").get("parameters")); 1017 } else if (noticeType.equals("ServerAlert")) { 1018 JSONArray actionURLs = notice.getJSONObject("data").getJSONArray("actionURLs"); 1019 ArrayList<String> actionURLsList = new ArrayList<String>(); 1020 for (int i=0; i<actionURLs.length(); i++) { 1021 actionURLsList.add(actionURLs.getString(i)); 1022 } 1023 mHostService.onServerAlert( 1024 notice.getJSONObject("data").getString("reason"), 1025 notice.getJSONObject("data").getString("subject"), 1026 actionURLsList); 1027 } 1028 1029 if (diagnostic) { 1030 String diagnosticMessage = noticeType + ": " + notice.getJSONObject("data").toString(); 1031 mHostService.onDiagnosticMessage(diagnosticMessage); 1032 } 1033 1034 } catch (JSONException e) { 1035 // Ignore notice 1036 } 1037 } 1038 1039 private static String setupTrustedCertificates(Context context, HostLogger logger) throws Exception { 1040 1041 // Copy the Android system CA store to a local, private cert bundle file. 1042 // 1043 // This results in a file that can be passed to SSL_CTX_load_verify_locations 1044 // for use with OpenSSL modes in tunnel-core. 1045 // https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_load_verify_locations.html 1046 // 1047 // TODO: to use the path mode of load_verify_locations would require emulating 1048 // the filename scheme used by c_rehash: 1049 // https://www.openssl.org/docs/manmaster/apps/c_rehash.html 1050 // http://stackoverflow.com/questions/19237167/the-new-subject-hash-openssl-algorithm-differs 1051 1052 File directory = context.getDir("PsiphonCAStore", Context.MODE_PRIVATE); 1053 1054 final String errorMessage = "copy AndroidCAStore failed"; 1055 try { 1056 1057 File file = new File(directory, "certs.dat"); 1058 1059 // Pave a fresh copy on every run, which ensures we're not using old certs. 1060 // Note: assumes KeyStore doesn't return revoked certs. 1061 // 1062 // TODO: this takes under 1 second, but should we avoid repaving every time? 1063 file.delete(); 1064 1065 PrintStream output = null; 1066 try { 1067 output = new PrintStream(new FileOutputStream(file)); 1068 1069 KeyStore keyStore; 1070 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1071 keyStore = KeyStore.getInstance("AndroidCAStore"); 1072 keyStore.load(null, null); 1073 } else { 1074 keyStore = KeyStore.getInstance("BKS"); 1075 FileInputStream inputStream = new FileInputStream("/etc/security/cacerts.bks"); 1076 try { 1077 keyStore.load(inputStream, "changeit".toCharArray()); 1078 } finally { 1079 if (inputStream != null) { 1080 inputStream.close(); 1081 } 1082 } 1083 } 1084 1085 Enumeration<String> aliases = keyStore.aliases(); 1086 while (aliases.hasMoreElements()) { 1087 String alias = aliases.nextElement(); 1088 X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); 1089 1090 output.println("-----BEGIN CERTIFICATE-----"); 1091 String pemCert = new String(Base64.encode(cert.getEncoded(), Base64.NO_WRAP), "UTF-8"); 1092 // OpenSSL appears to reject the default linebreaking done by Base64.encode, 1093 // so we manually linebreak every 64 characters 1094 for (int i = 0; i < pemCert.length() ; i+= 64) { 1095 output.println(pemCert.substring(i, Math.min(i + 64, pemCert.length()))); 1096 } 1097 output.println("-----END CERTIFICATE-----"); 1098 } 1099 1100 logger.onDiagnosticMessage("prepared PsiphonCAStore"); 1101 1102 return file.getAbsolutePath(); 1103 1104 } finally { 1105 if (output != null) { 1106 output.close(); 1107 } 1108 } 1109 1110 } catch (KeyStoreException e) { 1111 throw new Exception(errorMessage, e); 1112 } catch (NoSuchAlgorithmException e) { 1113 throw new Exception(errorMessage, e); 1114 } catch (CertificateException e) { 1115 throw new Exception(errorMessage, e); 1116 } catch (IOException e) { 1117 throw new Exception(errorMessage, e); 1118 } 1119 } 1120 1121 private static String getDeviceRegion(Context context) { 1122 String region = ""; 1123 TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); 1124 if (telephonyManager != null) { 1125 region = telephonyManager.getSimCountryIso(); 1126 if (region == null) { 1127 region = ""; 1128 } 1129 if (region.length() == 0 && telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { 1130 region = telephonyManager.getNetworkCountryIso(); 1131 if (region == null) { 1132 region = ""; 1133 } 1134 } 1135 } 1136 if (region.length() == 0) { 1137 Locale defaultLocale = Locale.getDefault(); 1138 if (defaultLocale != null) { 1139 region = defaultLocale.getCountry(); 1140 } 1141 } 1142 return region.toUpperCase(Locale.US); 1143 } 1144 1145 //---------------------------------------------------------------------------------------------- 1146 // Tun2Socks 1147 //---------------------------------------------------------------------------------------------- 1148 1149 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) 1150 private void startTun2Socks( 1151 final ParcelFileDescriptor vpnInterfaceFileDescriptor, 1152 final int vpnInterfaceMTU, 1153 final String vpnIpAddress, 1154 final String vpnNetMask, 1155 final String socksServerAddress, 1156 final String udpgwServerAddress, 1157 final boolean udpgwTransparentDNS) { 1158 if (mTun2SocksThread != null) { 1159 return; 1160 } 1161 mTun2SocksThread = new Thread(new Runnable() { 1162 @Override 1163 public void run() { 1164 runTun2Socks( 1165 vpnInterfaceFileDescriptor.detachFd(), 1166 vpnInterfaceMTU, 1167 vpnIpAddress, 1168 vpnNetMask, 1169 socksServerAddress, 1170 udpgwServerAddress, 1171 udpgwTransparentDNS ? 1 : 0); 1172 } 1173 }); 1174 mTun2SocksThread.start(); 1175 mHostService.onDiagnosticMessage("tun2socks started"); 1176 } 1177 1178 private void stopTun2Socks() { 1179 if (mTun2SocksThread != null) { 1180 try { 1181 terminateTun2Socks(); 1182 mTun2SocksThread.join(); 1183 } catch (InterruptedException e) { 1184 Thread.currentThread().interrupt(); 1185 } 1186 mTun2SocksThread = null; 1187 mHostService.onDiagnosticMessage("tun2socks stopped"); 1188 } 1189 } 1190 1191 public static void logTun2Socks(String level, String channel, String msg) { 1192 String logMsg = "tun2socks: " + level + "(" + channel + "): " + msg; 1193 mPsiphonTunnel.mHostService.onDiagnosticMessage(logMsg); 1194 } 1195 1196 private native static int runTun2Socks( 1197 int vpnInterfaceFileDescriptor, 1198 int vpnInterfaceMTU, 1199 String vpnIpAddress, 1200 String vpnNetMask, 1201 String socksServerAddress, 1202 String udpgwServerAddress, 1203 int udpgwTransparentDNS); 1204 1205 private native static int terminateTun2Socks(); 1206 1207 private native static int enableUdpGwKeepalive(); 1208 private native static int disableUdpGwKeepalive(); 1209 1210 //---------------------------------------------------------------------------------------------- 1211 // Implementation: Network Utils 1212 //---------------------------------------------------------------------------------------------- 1213 1214 private static boolean hasNetworkConnectivity(Context context) { 1215 ConnectivityManager connectivityManager = 1216 (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); 1217 if (connectivityManager == null) { 1218 return false; 1219 } 1220 NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 1221 return networkInfo != null && networkInfo.isConnected(); 1222 } 1223 1224 private static class PrivateAddress { 1225 final public String mIpAddress; 1226 final public String mSubnet; 1227 final public int mPrefixLength; 1228 final public String mRouter; 1229 public PrivateAddress(String ipAddress, String subnet, int prefixLength, String router) { 1230 mIpAddress = ipAddress; 1231 mSubnet = subnet; 1232 mPrefixLength = prefixLength; 1233 mRouter = router; 1234 } 1235 } 1236 1237 private static PrivateAddress selectPrivateAddress() throws Exception { 1238 // Select one of 10.0.0.1, 172.16.0.1, or 192.168.0.1 depending on 1239 // which private address range isn't in use. 1240 1241 Map<String, PrivateAddress> candidates = new HashMap<String, PrivateAddress>(); 1242 candidates.put( "10", new PrivateAddress("10.0.0.1", "10.0.0.0", 8, "10.0.0.2")); 1243 candidates.put("172", new PrivateAddress("172.16.0.1", "172.16.0.0", 12, "172.16.0.2")); 1244 candidates.put("192", new PrivateAddress("192.168.0.1", "192.168.0.0", 16, "192.168.0.2")); 1245 candidates.put("169", new PrivateAddress("169.254.1.1", "169.254.1.0", 24, "169.254.1.2")); 1246 1247 Enumeration<NetworkInterface> netInterfaces; 1248 try { 1249 netInterfaces = NetworkInterface.getNetworkInterfaces(); 1250 } catch (SocketException e) { 1251 throw new Exception("selectPrivateAddress failed", e); 1252 } 1253 1254 if (netInterfaces == null) { 1255 throw new Exception("no network interfaces found"); 1256 } 1257 1258 for (NetworkInterface netInterface : Collections.list(netInterfaces)) { 1259 for (InetAddress inetAddress : Collections.list(netInterface.getInetAddresses())) { 1260 if (inetAddress instanceof Inet4Address) { 1261 String ipAddress = inetAddress.getHostAddress(); 1262 if (ipAddress.startsWith("10.")) { 1263 candidates.remove("10"); 1264 } 1265 else if ( 1266 ipAddress.length() >= 6 && 1267 ipAddress.substring(0, 6).compareTo("172.16") >= 0 && 1268 ipAddress.substring(0, 6).compareTo("172.31") <= 0) { 1269 candidates.remove("172"); 1270 } 1271 else if (ipAddress.startsWith("192.168")) { 1272 candidates.remove("192"); 1273 } 1274 } 1275 } 1276 } 1277 1278 if (candidates.size() > 0) { 1279 return candidates.values().iterator().next(); 1280 } 1281 1282 throw new Exception("no private address available"); 1283 } 1284 1285 private static Collection<String> getActiveNetworkDNSServers(Context context, boolean isVpnMode) 1286 throws Exception { 1287 1288 ArrayList<String> servers = new ArrayList<String>(); 1289 for (InetAddress serverAddress : getActiveNetworkDNSServerAddresses(context, isVpnMode)) { 1290 String server = serverAddress.toString(); 1291 // strip the leading slash e.g., "/192.168.1.1" 1292 if (server.startsWith("/")) { 1293 server = server.substring(1); 1294 } 1295 servers.add(server); 1296 } 1297 1298 if (servers.isEmpty()) { 1299 throw new Exception("no active network DNS resolver"); 1300 } 1301 1302 return servers; 1303 } 1304 1305 private static Collection<InetAddress> getActiveNetworkDNSServerAddresses(Context context, boolean isVpnMode) 1306 throws Exception { 1307 1308 final String errorMessage = "getActiveNetworkDNSServerAddresses failed"; 1309 ArrayList<InetAddress> dnsAddresses = new ArrayList<InetAddress>(); 1310 1311 ConnectivityManager connectivityManager = 1312 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 1313 if (connectivityManager == null) { 1314 throw new Exception(errorMessage, new Throwable("couldn't get ConnectivityManager system service")); 1315 } 1316 1317 try { 1318 1319 // Hidden API: 1320 // 1321 // - Only available in Android 4.0+ 1322 // - No guarantee will be available beyond 4.2, or on all vendor 1323 // devices 1324 // - Field reports indicate this is no longer working on some -- 1325 // but not all -- Android 10+ devices 1326 1327 Class<?> LinkPropertiesClass = Class.forName("android.net.LinkProperties"); 1328 Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{}); 1329 Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager); 1330 if (linkProperties != null) { 1331 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 1332 Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{}); 1333 Collection<?> dnses = (Collection<?>)getDnsesMethod.invoke(linkProperties); 1334 for (Object dns : dnses) { 1335 dnsAddresses.add((InetAddress)dns); 1336 } 1337 } else { 1338 // LinkProperties is public in API 21 (and the DNS function signature has changed) 1339 for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) { 1340 dnsAddresses.add(dns); 1341 } 1342 } 1343 } 1344 } catch (ClassNotFoundException e) { 1345 } catch (NoSuchMethodException e) { 1346 } catch (IllegalArgumentException e) { 1347 } catch (IllegalAccessException e) { 1348 } catch (InvocationTargetException e) { 1349 } catch (NullPointerException e) { 1350 } 1351 1352 if (!dnsAddresses.isEmpty()) { 1353 return dnsAddresses; 1354 } 1355 1356 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 1357 1358 // This case is attempted only when the hidden API fails: 1359 // 1360 // - Testing shows the hidden API still works more reliably on 1361 // some Android 11+ devices 1362 // - Testing indicates that the NetworkRequest can sometimes 1363 // select the wrong network 1364 // - e.g., mobile instead of WiFi, and return the wrong DNS 1365 // servers 1366 // - there's currently no way to filter for the "currently 1367 // active default data network" returned by, e.g., the 1368 // deprecated getActiveNetworkInfo 1369 // - we cannot add the NET_CAPABILITY_FOREGROUND capability to 1370 // the NetworkRequest at this time due to target SDK 1371 // constraints 1372 1373 NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder() 1374 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); 1375 1376 if (isVpnMode) { 1377 // In VPN mode, we want the DNS servers for the underlying physical network. 1378 networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); 1379 } 1380 1381 NetworkRequest networkRequest = networkRequestBuilder.build(); 1382 1383 final CountDownLatch countDownLatch = new CountDownLatch(1); 1384 try { 1385 ConnectivityManager.NetworkCallback networkCallback = 1386 new ConnectivityManager.NetworkCallback() { 1387 @Override 1388 public void onLinkPropertiesChanged(Network network, 1389 LinkProperties linkProperties) { 1390 dnsAddresses.addAll(linkProperties.getDnsServers()); 1391 countDownLatch.countDown(); 1392 } 1393 }; 1394 1395 connectivityManager.registerNetworkCallback(networkRequest, networkCallback); 1396 countDownLatch.await(1, TimeUnit.SECONDS); 1397 connectivityManager.unregisterNetworkCallback(networkCallback); 1398 } catch (RuntimeException ignored) { 1399 // Failed to register network callback 1400 } catch (InterruptedException e) { 1401 Thread.currentThread().interrupt(); 1402 } 1403 } 1404 1405 return dnsAddresses; 1406 } 1407 1408 private static boolean hasIPv6Route(Context context) throws Exception { 1409 1410 try { 1411 // This logic mirrors the logic in 1412 // psiphon/common/resolver.hasRoutableIPv6Interface. That 1413 // function currently doesn't work on Android due to Go's 1414 // net.InterfaceAddrs failing on Android SDK 30+ (see Go issue 1415 // 40569). hasIPv6Route provides the same functionality via a 1416 // callback into Java code. 1417 1418 for (NetworkInterface netInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) { 1419 if (netInterface.isUp() && 1420 !netInterface.isLoopback() && 1421 !netInterface.isPointToPoint()) { 1422 for (InetAddress address : Collections.list(netInterface.getInetAddresses())) { 1423 1424 // Per https://developer.android.com/reference/java/net/Inet6Address#textual-representation-of-ip-addresses, 1425 // "Java will never return an IPv4-mapped address. 1426 // These classes can take an IPv4-mapped address as 1427 // input, both in byte array and text 1428 // representation. However, it will be converted 1429 // into an IPv4 address." As such, when the type of 1430 // the IP address is Inet6Address, this should be 1431 // an actual IPv6 address. 1432 1433 if (address instanceof Inet6Address && 1434 !address.isLinkLocalAddress() && 1435 !address.isSiteLocalAddress() && 1436 !address.isMulticastAddress ()) { 1437 return true; 1438 } 1439 } 1440 } 1441 } 1442 } catch (SocketException e) { 1443 throw new Exception("hasIPv6Route failed", e); 1444 } 1445 1446 return false; 1447 } 1448 1449 //---------------------------------------------------------------------------------------------- 1450 // Exception 1451 //---------------------------------------------------------------------------------------------- 1452 1453 public static class Exception extends java.lang.Exception { 1454 private static final long serialVersionUID = 1L; 1455 public Exception(String message) { 1456 super(message); 1457 } 1458 public Exception(String message, Throwable cause) { 1459 super(message + ": " + cause.getMessage()); 1460 } 1461 } 1462 1463 //---------------------------------------------------------------------------------------------- 1464 // Network connectivity monitor 1465 //---------------------------------------------------------------------------------------------- 1466 1467 private static class NetworkMonitor { 1468 private final NetworkChangeListener listener; 1469 private ConnectivityManager.NetworkCallback networkCallback; 1470 private AtomicReference<String> activeNetworkType; 1471 private AtomicReference<String> activeNetworkDNSServers; 1472 private HostLogger logger; 1473 1474 public NetworkMonitor( 1475 NetworkChangeListener listener, 1476 AtomicReference<String> activeNetworkType, 1477 AtomicReference<String> activeNetworkDNSServers, 1478 HostLogger logger) { 1479 1480 this.listener = listener; 1481 this.activeNetworkType = activeNetworkType; 1482 this.activeNetworkDNSServers = activeNetworkDNSServers; 1483 this.logger = logger; 1484 } 1485 1486 private void start(Context context) { 1487 // Need API 21(LOLLIPOP)+ for ConnectivityManager.NetworkCallback 1488 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 1489 return; 1490 } 1491 ConnectivityManager connectivityManager = 1492 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 1493 if (connectivityManager == null) { 1494 return; 1495 } 1496 networkCallback = new ConnectivityManager.NetworkCallback() { 1497 private boolean isInitialState = true; 1498 private Network currentActiveNetwork; 1499 1500 private void consumeActiveNetwork(Network network) { 1501 if (isInitialState) { 1502 isInitialState = false; 1503 setCurrentActiveNetworkAndProperties(network); 1504 return; 1505 } 1506 1507 if (!network.equals(currentActiveNetwork)) { 1508 setCurrentActiveNetworkAndProperties(network); 1509 if (listener != null) { 1510 listener.onChanged(); 1511 } 1512 } 1513 } 1514 1515 private void consumeLostNetwork(Network network) { 1516 if (network.equals(currentActiveNetwork)) { 1517 setCurrentActiveNetworkAndProperties(null); 1518 if (listener != null) { 1519 listener.onChanged(); 1520 } 1521 } 1522 } 1523 1524 private void setCurrentActiveNetworkAndProperties(Network network) { 1525 1526 currentActiveNetwork = network; 1527 1528 if (network == null) { 1529 1530 activeNetworkType.set("NONE"); 1531 activeNetworkDNSServers.set(""); 1532 logger.onDiagnosticMessage("NetworkMonitor: clear current active network"); 1533 1534 } else { 1535 1536 String networkType = "UNKNOWN"; 1537 try { 1538 // Limitation: a network may have both CELLULAR 1539 // and WIFI transports, or different network 1540 // transport types entirely. This logic currently 1541 // mimics the type determination logic in 1542 // getNetworkID. 1543 NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); 1544 if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { 1545 networkType = "MOBILE"; 1546 } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { 1547 networkType = "WIFI"; 1548 } 1549 } catch (java.lang.Exception e) { 1550 } 1551 activeNetworkType.set(networkType); 1552 1553 ArrayList<String> servers = new ArrayList<String>(); 1554 try { 1555 LinkProperties linkProperties = connectivityManager.getLinkProperties(network); 1556 List<InetAddress> serverAddresses = linkProperties.getDnsServers(); 1557 for (InetAddress serverAddress : serverAddresses) { 1558 String server = serverAddress.toString(); 1559 if (server.startsWith("/")) { 1560 server = server.substring(1); 1561 } 1562 servers.add(server); 1563 } 1564 } catch (java.lang.Exception e) { 1565 } 1566 // Use the workaround, comma-delimited format required for gobind. 1567 activeNetworkDNSServers.set(String.join(",", servers)); 1568 1569 String message = "NetworkMonitor: set current active network " + networkType; 1570 if (!servers.isEmpty()) { 1571 // The DNS server address is potential PII and not logged. 1572 message += " with DNS"; 1573 } 1574 logger.onDiagnosticMessage(message); 1575 } 1576 } 1577 1578 @Override 1579 public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) { 1580 super.onCapabilitiesChanged(network, capabilities); 1581 1582 // Need API 23(M)+ for NET_CAPABILITY_VALIDATED 1583 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1584 return; 1585 } 1586 1587 // https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED 1588 // Indicates that connectivity on this network was successfully validated. 1589 // For example, for a network with NET_CAPABILITY_INTERNET, it means that Internet connectivity was 1590 // successfully detected. 1591 if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { 1592 consumeActiveNetwork(network); 1593 } 1594 } 1595 1596 @Override 1597 public void onAvailable(Network network) { 1598 super.onAvailable(network); 1599 1600 // Skip on API 26(O)+ because onAvailable is guaranteed to be followed by 1601 // onCapabilitiesChanged 1602 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 1603 return; 1604 } 1605 consumeActiveNetwork(network); 1606 } 1607 1608 @Override 1609 public void onLost(Network network) { 1610 super.onLost(network); 1611 consumeLostNetwork(network); 1612 } 1613 }; 1614 1615 try { 1616 // When searching for a network to satisfy a request, all capabilities requested must be satisfied. 1617 NetworkRequest.Builder builder = new NetworkRequest.Builder() 1618 // Indicates that this network should be able to reach the internet. 1619 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); 1620 1621 if (mPsiphonTunnel.mVpnMode.get()) { 1622 // If we are in the VPN mode then ensure we monitor only the VPN's underlying 1623 // active networks and not self. 1624 builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); 1625 } else { 1626 // If we are NOT in the VPN mode then monitor default active networks with the 1627 // Internet capability, including VPN, to ensure we won't trigger a reconnect in 1628 // case the VPN is up while the system switches the underlying network. 1629 builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); 1630 } 1631 1632 NetworkRequest networkRequest = builder.build(); 1633 connectivityManager.requestNetwork(networkRequest, networkCallback); 1634 } catch (RuntimeException ignored) { 1635 // Could be a security exception or any other runtime exception on customized firmwares. 1636 networkCallback = null; 1637 } 1638 } 1639 1640 private void stop(Context context) { 1641 if (networkCallback == null) { 1642 return; 1643 } 1644 // Need API 21(LOLLIPOP)+ for ConnectivityManager.NetworkCallback 1645 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 1646 return; 1647 } 1648 ConnectivityManager connectivityManager = 1649 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 1650 if (connectivityManager == null) { 1651 return; 1652 } 1653 // Note: ConnectivityManager.unregisterNetworkCallback() may throw 1654 // "java.lang.IllegalArgumentException: NetworkCallback was not registered". 1655 // This scenario should be handled in the start() above but we'll add a try/catch 1656 // anyway to match the start's call to ConnectivityManager.registerNetworkCallback() 1657 try { 1658 connectivityManager.unregisterNetworkCallback(networkCallback); 1659 } catch (RuntimeException ignored) { 1660 } 1661 networkCallback = null; 1662 } 1663 1664 public interface NetworkChangeListener { 1665 void onChanged(); 1666 } 1667 } 1668 }