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