/** * Copyright (c) 2013 LEAP Encryption Access Project and contributers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package se.leap.bitmaskclient.eip; import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.annotation.WorkerThread; import android.support.v4.app.JobIntentService; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.io.Closeable; import java.lang.ref.WeakReference; import java.util.Observable; import java.util.Observer; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.IOpenVPNServiceInternal; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.VpnStatus; import de.blinkt.openvpn.core.connection.Connection; import se.leap.bitmaskclient.OnBootReceiver; import se.leap.bitmaskclient.R; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; import static android.content.Intent.CATEGORY_DEFAULT; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN; import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT; import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; import static se.leap.bitmaskclient.Constants.EIP_ACTION_IS_RUNNING; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP; import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN; import static se.leap.bitmaskclient.Constants.EIP_EARLY_ROUTES; import static se.leap.bitmaskclient.Constants.EIP_N_CLOSEST_GATEWAY; import static se.leap.bitmaskclient.Constants.EIP_RECEIVER; import static se.leap.bitmaskclient.Constants.EIP_REQUEST; import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE; import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; import static se.leap.bitmaskclient.MainActivityErrorDialog.DOWNLOAD_ERRORS.ERROR_INVALID_VPN_CERTIFICATE; import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid; import static se.leap.bitmaskclient.utils.ConfigHelper.ensureNotOnMainThread; import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; /** * EIP is the abstract base class for interacting with and managing the Encrypted * Internet Proxy connection. Connections are started, stopped, and queried through * this Service. * Contains logic for parsing eip-service.json from the provider, configuring and selecting * gateways, and controlling {@link de.blinkt.openvpn.core.OpenVPNService} connections. * * @author Sean Leonard * @author Parménides GV */ public final class EIP extends JobIntentService implements Observer { public final static String TAG = EIP.class.getSimpleName(), SERVICE_API_PATH = "config/eip-service.json", ERRORS = "errors", ERROR_ID = "errorID"; private volatile SharedPreferences preferences; private volatile EipStatus eipStatus; // Service connection to OpenVpnService, shared between threads private volatile OpenVpnServiceConnection openVpnServiceConnection; private WeakReference mResultRef = new WeakReference<>(null); /** * Unique job ID for this service. */ static final int JOB_ID = 1312; /** * Convenience method for enqueuing work in to this service. */ static void enqueueWork(Context context, Intent work) { enqueueWork(context, EIP.class, JOB_ID, work); } @Override public void onCreate() { super.onCreate(); eipStatus = EipStatus.getInstance(); eipStatus.addObserver(this); preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); } @Override public void onDestroy() { super.onDestroy(); eipStatus.deleteObserver(this); if (openVpnServiceConnection != null) { openVpnServiceConnection.close(); openVpnServiceConnection = null; } } /** * update eipStatus whenever it changes */ @Override public void update(Observable observable, Object data) { if (observable instanceof EipStatus) { eipStatus = (EipStatus) observable; } } /** * * @param intent the intent that started this EIP call */ @Override protected void onHandleWork(@NonNull Intent intent) { final String action = intent.getAction(); if (action == null) { return; } if (intent.getParcelableExtra(EIP_RECEIVER) != null) { mResultRef = new WeakReference<>((ResultReceiver) intent.getParcelableExtra(EIP_RECEIVER)); } int nClosestGateway; switch (action) { case EIP_ACTION_START: boolean earlyRoutes = intent.getBooleanExtra(EIP_EARLY_ROUTES, true); nClosestGateway = intent.getIntExtra(EIP_N_CLOSEST_GATEWAY, 0); startEIP(earlyRoutes, nClosestGateway); break; case EIP_ACTION_START_ALWAYS_ON_VPN: startEIPAlwaysOnVpn(); break; case EIP_ACTION_STOP: stopEIP(); break; case EIP_ACTION_IS_RUNNING: isRunning(); break; case EIP_ACTION_CHECK_CERT_VALIDITY: checkVPNCertificateValidity(); break; } } /** * Initiates an EIP connection by selecting a gateway and preparing and sending an * Intent to {@link de.blinkt.openvpn.LaunchVPN}. * It also sets up early routes. * @param earlyRoutes if true, a void vpn gets set up * @param nClosestGateway the gateway that is the n nearest one to the users place */ @SuppressLint("ApplySharedPref") private void startEIP(boolean earlyRoutes, int nClosestGateway) { Log.d(TAG, "start EIP with early routes: " + earlyRoutes + " and nClosest Gateway: " + nClosestGateway); if (!eipStatus.isBlockingVpnEstablished() && earlyRoutes) { earlyRoutes(); } Bundle result = new Bundle(); if (!preferences.getBoolean(EIP_RESTART_ON_BOOT, false)) { preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, true).commit(); } if (!isVPNCertificateValid()) { setErrorResult(result, vpn_certificate_is_invalid, ERROR_INVALID_VPN_CERTIFICATE.toString()); tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED, result); return; } GatewaysManager gatewaysManager = gatewaysFromPreferences(); Gateway gateway = gatewaysManager.select(nClosestGateway); if (launchActiveGateway(gateway, nClosestGateway)) { tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_OK); } else { tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED); } } /** * Tries to start the last used vpn profile when the OS was rebooted and always-on-VPN is enabled. * The {@link OnBootReceiver} will care if there is no profile. */ private void startEIPAlwaysOnVpn() { GatewaysManager gatewaysManager = gatewaysFromPreferences(); Gateway gateway = gatewaysManager.select(0); if (!launchActiveGateway(gateway, 0)) { Log.d(TAG, "startEIPAlwaysOnVpn no active profile available!"); } } /** * Early routes are routes that block traffic until a new * VpnService is started properly. */ private void earlyRoutes() { Intent voidVpnLauncher = new Intent(getApplicationContext(), VoidVpnLauncher.class); voidVpnLauncher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(voidVpnLauncher); } /** * starts the VPN and connects to the given gateway * * @param gateway to connect to */ private boolean launchActiveGateway(Gateway gateway, int nClosestGateway) { VpnProfile profile; Connection.TransportType transportType = getUsePluggableTransports(this) ? OBFS4 : OPENVPN; if (gateway == null || (profile = gateway.getProfile(transportType)) == null) { return false; } Intent intent = new Intent(BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT); intent.putExtra(PROVIDER_PROFILE, profile); intent.putExtra(Gateway.KEY_N_CLOSEST_GATEWAY, nClosestGateway); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); return true; } /** * Stop VPN * First checks if the OpenVpnConnection is open then * terminates EIP if currently connected or connecting */ private void stopEIP() { VpnStatus.updateStateString("STOPPING", "STOPPING VPN", R.string.state_exiting, ConnectionStatus.LEVEL_STOPPING); int resultCode = stop() ? RESULT_OK : RESULT_CANCELED; tellToReceiverOrBroadcast(EIP_ACTION_STOP, resultCode); } /** * Checks the last stored status notified by ics-openvpn * Sends Activity.RESULT_CANCELED to the ResultReceiver that made the * request if it's not connected, Activity.RESULT_OK otherwise. */ private void isRunning() { int resultCode = (eipStatus.isConnected()) ? RESULT_OK : RESULT_CANCELED; tellToReceiverOrBroadcast(EIP_ACTION_IS_RUNNING, resultCode); } /** * read eipServiceJson from preferences and parse Gateways * * @return GatewaysManager */ private GatewaysManager gatewaysFromPreferences() { GatewaysManager gatewaysManager = new GatewaysManager(preferences); gatewaysManager.configureFromPreferences(); return gatewaysManager; } /** * read VPN certificate from preferences and check it * broadcast result */ private void checkVPNCertificateValidity() { int resultCode = isVPNCertificateValid() ? RESULT_OK : RESULT_CANCELED; tellToReceiverOrBroadcast(EIP_ACTION_CHECK_CERT_VALIDITY, resultCode); } /** * read VPN certificate from preferences and check it * * @return true if VPN certificate is valid false otherwise */ private boolean isVPNCertificateValid() { VpnCertificateValidator validator = new VpnCertificateValidator(preferences.getString(PROVIDER_VPN_CERTIFICATE, "")); return validator.isValid(); } /** * send resultCode and resultData to receiver or * broadcast the result if no receiver is defined * * @param action the action that has been performed * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise * @param resultData other data to broadcast or return to receiver */ private void tellToReceiverOrBroadcast(String action, int resultCode, Bundle resultData) { resultData.putString(EIP_REQUEST, action); if (mResultRef.get() != null) { mResultRef.get().send(resultCode, resultData); } else { broadcastEvent(resultCode, resultData); } } /** * send resultCode and resultData to receiver or * broadcast the result if no receiver is defined * * @param action the action that has been performed * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise */ private void tellToReceiverOrBroadcast(String action, int resultCode) { tellToReceiverOrBroadcast(action, resultCode, new Bundle()); } /** * broadcast result * * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise * @param resultData other data to broadcast or return to receiver */ private void broadcastEvent(int resultCode, Bundle resultData) { Intent intentUpdate = new Intent(BROADCAST_EIP_EVENT); intentUpdate.addCategory(CATEGORY_DEFAULT); intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); Log.d(TAG, "sending broadcast"); LocalBroadcastManager.getInstance(this).sendBroadcast(intentUpdate); } /** * helper function to add error to result bundle * * @param result - result of an action * @param errorMessageId - id of string resource describing the error * @param errorId - MainActivityErrorDialog DownloadError id */ void setErrorResult(Bundle result, @StringRes int errorMessageId, String errorId) { JSONObject errorJson = new JSONObject(); try { errorJson.put(ERRORS, getResources().getString(errorMessageId)); errorJson.put(ERROR_ID, errorId); } catch (JSONException e) { e.printStackTrace(); } result.putString(ERRORS, errorJson.toString()); result.putBoolean(BROADCAST_RESULT_KEY, false); } /** * disable Bitmask starting on after phone reboot * then stop VPN */ private boolean stop() { preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, false).apply(); if (eipStatus.isBlockingVpnEstablished()) { stopBlockingVpn(); } return disconnect(); } /** * stop void vpn from blocking internet */ private void stopBlockingVpn() { Log.d(TAG, "stop VoidVpn!"); Intent stopVoidVpnIntent = new Intent(this, VoidVpnService.class); stopVoidVpnIntent.setAction(EIP_ACTION_STOP_BLOCKING_VPN); startService(stopVoidVpnIntent); } /** * creates a OpenVpnServiceConnection if necessary * then terminates OpenVPN */ private boolean disconnect() { try { initOpenVpnServiceConnection(); } catch (InterruptedException | IllegalStateException e) { return false; } try { return openVpnServiceConnection.getService().stopVPN(false); } catch (RemoteException e) { VpnStatus.logException(e); } return false; } /** * Assigns a new OpenVpnServiceConnection to EIP's member variable openVpnServiceConnection. * Only one thread at a time can create the service connection, that will be shared between threads * * @throws InterruptedException thrown if thread gets interrupted * @throws IllegalStateException thrown if this method was not called from a background thread */ private void initOpenVpnServiceConnection() throws InterruptedException, IllegalStateException { if (openVpnServiceConnection == null) { Log.d(TAG, "serviceConnection is still null"); openVpnServiceConnection = new OpenVpnServiceConnection(this); } } /** * Creates a service connection to OpenVpnService. * The constructor blocks until the service is bound to the given Context. * Pattern stolen from android.security.KeyChain.java */ @WorkerThread public static class OpenVpnServiceConnection implements Closeable { private final Context context; private ServiceConnection serviceConnection; private IOpenVPNServiceInternal service; OpenVpnServiceConnection(Context context) throws InterruptedException, IllegalStateException { this.context = context; ensureNotOnMainThread(context); Log.d(TAG, "initSynchronizedServiceConnection!"); initSynchronizedServiceConnection(context); } private void initSynchronizedServiceConnection(final Context context) throws InterruptedException { final BlockingQueue blockingQueue = new LinkedBlockingQueue<>(1); this.serviceConnection = new ServiceConnection() { volatile boolean mConnectedAtLeastOnce = false; @Override public void onServiceConnected(ComponentName name, IBinder service) { if (!mConnectedAtLeastOnce) { mConnectedAtLeastOnce = true; try { blockingQueue.put(IOpenVPNServiceInternal.Stub.asInterface(service)); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void onServiceDisconnected(ComponentName name) { } }; Intent intent = new Intent(context, OpenVPNService.class); intent.setAction(OpenVPNService.START_SERVICE); context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); service = blockingQueue.take(); } @Override public void close() { context.unbindService(serviceConnection); } public IOpenVPNServiceInternal getService() { return service; } } }