From ca1cad6ec5b175a85b361c45e8d2c0cac0b405ec Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 7 Dec 2017 12:49:15 +0100 Subject: #8742 basic always-on implementation with blocking vpn if no profile is configured --- .../openvpn/core/IOpenVPNServiceInternal.aidl | 2 + .../de/blinkt/openvpn/core/ConnectionStatus.java | 1 + .../de/blinkt/openvpn/core/OpenVPNService.java | 20 +++ .../main/java/se/leap/bitmaskclient/Dashboard.java | 134 +++++++++++----- .../java/se/leap/bitmaskclient/OnBootReceiver.java | 43 +++-- .../java/se/leap/bitmaskclient/VpnFragment.java | 142 ++++++++++------- .../java/se/leap/bitmaskclient/eip/Constants.java | 3 + .../main/java/se/leap/bitmaskclient/eip/EIP.java | 30 +++- .../java/se/leap/bitmaskclient/eip/EipStatus.java | 175 ++++++++++++++++----- .../se/leap/bitmaskclient/eip/VoidVpnService.java | 100 +++++++++--- .../bitmaskclient/userstatus/SessionDialog.java | 2 +- .../userstatus/UserStatusFragment.java | 3 +- app/src/main/res/values/strings.xml | 5 + 13 files changed, 492 insertions(+), 168 deletions(-) diff --git a/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl b/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl index 3958bcf3..b19cf99e 100644 --- a/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl +++ b/app/src/main/aidl/de/blinkt/openvpn/core/IOpenVPNServiceInternal.aidl @@ -20,4 +20,6 @@ interface IOpenVPNServiceInternal { * @return true if there was a process that has been send a stop signal */ boolean stopVPN(boolean replaceConnection); + + boolean isVpnRunning(); } diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java b/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java index 03d842e3..3e6d23f7 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java +++ b/app/src/main/java/de/blinkt/openvpn/core/ConnectionStatus.java @@ -21,6 +21,7 @@ public enum ConnectionStatus implements Parcelable { LEVEL_START, LEVEL_AUTH_FAILED, LEVEL_WAITING_FOR_USER_INPUT, + LEVEL_BLOCKING, // used for Bitmask's VoidVPN UNKNOWN_LEVEL; @Override diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index ded7490a..86e1bb02 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -115,6 +115,11 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac public boolean stopVPN(boolean replaceConnection) throws RemoteException { return OpenVPNService.this.stopVPN(replaceConnection); } + + @Override + public boolean isVpnRunning() throws RemoteException { + return OpenVPNService.this.isVpnRunning(); + } }; // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java @@ -412,6 +417,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } + @Override public void userPause(boolean shouldBePaused) { if (mDeviceStateReceiver != null) mDeviceStateReceiver.userPause(shouldBePaused); @@ -425,6 +431,20 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac return false; } + /** + * used in Bitmask + */ + @Override + public boolean isVpnRunning() { + boolean hasVPNProcessThread = false; + synchronized (mProcessLock) { + hasVPNProcessThread = mProcessThread != null && mProcessThread.isAlive(); + } + + return hasVPNProcessThread; + + } + @Override public int onStartCommand(Intent intent, int flags, int startId) { diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java index 3f1663d0..1a4adc1d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java +++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java @@ -63,6 +63,9 @@ import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatusFragment; +import static se.leap.bitmaskclient.eip.Constants.IS_ALWAYS_ON; +import static se.leap.bitmaskclient.eip.Constants.RESTART_ON_BOOT; + /** * The main user facing Activity of Bitmask Android, consisting of status, controls, * and access to preferences. @@ -78,15 +81,23 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec public static final String TAG = Dashboard.class.getSimpleName(); public static final String SHARED_PREFERENCES = "LEAPPreferences"; public static final String ACTION_QUIT = "quit"; + + /** + * When "Disconnect" is clicked from the notification this extra gets added to the calling intent. + */ public static final String ACTION_ASK_TO_CANCEL_VPN = "ask to cancel vpn"; + /** + * if always-on feature is enabled, but there's no provider configured the EIP Service + * adds this intent extra. ACTION_CONFIGURE_ALWAYS_ON_PROFILE + * serves to start the Configuration Wizard on top of the Dashboard Activity. + */ + public static final String ACTION_CONFIGURE_ALWAYS_ON_PROFILE = "configure always-on profile"; public static final String REQUEST_CODE = "request_code"; public static final String PARAMETERS = "dashboard parameters"; - public static final String START_ON_BOOT = "dashboard start on boot"; - //FIXME: remove OR FIX ON_BOOT - public static final String ON_BOOT = "dashboard on boot"; public static final String APP_VERSION = "bitmask version"; - private static Context app; + //FIXME: context classes in static fields lead to memory leaks! + private static Context dashboardContext; protected static SharedPreferences preferences; private FragmentManagerEnhanced fragment_manager; @@ -108,31 +119,21 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec ProviderAPICommand.initialize(this); providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler(), this); - if (app == null) { - app = this; + if (dashboardContext == null) { + dashboardContext = this; VpnStatus.initLogCache(getApplicationContext().getCacheDir()); handleVersion(); User.init(getString(R.string.default_username)); } - boolean provider_exists = previousProviderExists(savedInstanceState); - if (provider_exists) { - provider = getProvider(savedInstanceState); - if(!provider.isConfigured()) - startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); - else { - buildDashboard(getIntent().getBooleanExtra(ON_BOOT, false)); - user_status_fragment.restoreSessionStatus(savedInstanceState); - } - } else { - startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); - } + + prepareEIP(savedInstanceState); } @Override protected void onResume() { super.onResume(); - handleVPNCancellation(getIntent()); + handleVpnCancellation(getIntent()); } private boolean previousProviderExists(Bundle savedInstanceState) { @@ -198,7 +199,19 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); - handleVPNCancellation(intent); + handleIntentExtras(intent); + } + + private void handleIntentExtras(Intent intent) { + if (intent.hasExtra(ACTION_ASK_TO_CANCEL_VPN)) { + handleVpnCancellation(intent); + } else if (intent.hasExtra(RESTART_ON_BOOT)) { + Log.d(TAG, "Dashboard: RESTART_ON_BOOT"); + prepareEIP(null); + } else if (intent.hasExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) { + Log.d(TAG, "Dashboard: ACTION_CONFIGURE_ALWAYS_ON_PROFILE"); + handleConfigureAlwaysOn(getIntent()); + } } @Override @@ -221,15 +234,44 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec } } - private void handleVPNCancellation(Intent intent) { + private void handleVpnCancellation(Intent intent) { if (intent.hasExtra(Dashboard.ACTION_ASK_TO_CANCEL_VPN)) { eip_fragment.askToStopEIP(); intent.removeExtra(ACTION_ASK_TO_CANCEL_VPN); } } + private void handleConfigureAlwaysOn(Intent intent) { + intent.removeExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE); + Log.d(TAG, "start Configuration wizard!"); + startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); + } + + private void prepareEIP(Bundle savedInstanceState) { + boolean provider_exists = previousProviderExists(savedInstanceState); + if (provider_exists) { + provider = getProvider(savedInstanceState); + if(!provider.isConfigured()) { + configureLeapProvider(); + } else { + Log.d(TAG, "vpn provider is configured"); + buildDashboard(getIntent().getBooleanExtra(RESTART_ON_BOOT, false)); + user_status_fragment.restoreSessionStatus(savedInstanceState); + } + } else { + configureLeapProvider(); + } + } + + private void configureLeapProvider() { + if (getIntent().hasExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) { + getIntent().removeExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE); + } + startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); + } @SuppressLint("CommitPrefEdits") private void providerToPreferences(Provider provider) { + //FIXME: figure out why .commit() is used and try refactor that cause, currently runs on UI thread preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).commit(); preferences.edit().putString(Provider.MAIN_URL, provider.mainUrl().toString()).apply(); preferences.edit().putString(Provider.KEY, provider.definition().toString()).apply(); @@ -261,7 +303,10 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec * Inflates permanent UI elements of the View and contains logic for what * service dependent UI elements to include. */ - private void buildDashboard(boolean hide_and_turn_on_eip) { + //TODO: REFACTOR ME! Consider implementing a manager that handles most of VpnFragment's logic about handling EIP commands. + //This way, we could avoid to create UI elements (like fragment_manager.replace(R.id.servicesCollection, eip_fragment, VpnFragment.TAG); ) + // just to start services and destroy them afterwards + private void buildDashboard(boolean hideAndTurnOnEipOnBoot) { setContentView(R.layout.dashboard); ButterKnife.inject(this); @@ -275,24 +320,43 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec if (provider.hasEIP()) { fragment_manager.removePreviousFragment(VpnFragment.TAG); - eip_fragment = new VpnFragment(); - - if (hide_and_turn_on_eip) { - //TODO: remove line below if not in use anymore... - preferences.edit().remove(Dashboard.START_ON_BOOT).apply(); - //FIXME: always start on Boot? Why do we keep shared preferences then? - Bundle arguments = new Bundle(); - arguments.putBoolean(VpnFragment.START_ON_BOOT, true); - if (eip_fragment != null) eip_fragment.setArguments(arguments); - } - + eip_fragment = prepareEipFragment(hideAndTurnOnEipOnBoot); fragment_manager.replace(R.id.servicesCollection, eip_fragment, VpnFragment.TAG); - if (hide_and_turn_on_eip) { + if (hideAndTurnOnEipOnBoot) { onBackPressed(); } } } + /** + * + * @param hideAndTurnOnEipOnBoot Flag that indicates if system intent android.intent.action.BOOT_COMPLETED + * has caused to start Dashboard + * @return + */ + private VpnFragment prepareEipFragment(boolean hideAndTurnOnEipOnBoot) { + VpnFragment eip_fragment = new VpnFragment(); + + if (hideAndTurnOnEipOnBoot && !isAlwaysOn()) { + preferences.edit().remove(Constants.RESTART_ON_BOOT).apply(); + Bundle arguments = new Bundle(); + arguments.putBoolean(VpnFragment.START_EIP_ON_BOOT, true); + Log.d(TAG, "set START_EIP_ON_BOOT argument for eip_fragment"); + eip_fragment.setArguments(arguments); + + } + return eip_fragment; + } + + /** + * checks if Android's VPN feature 'always-on' is enabled for Bitmask + * @return + */ + private boolean isAlwaysOn() { + return preferences.getBoolean(IS_ALWAYS_ON, false); + } + + @Override public boolean onPrepareOptionsMenu(Menu menu) { if (provider.allowsRegistration()) { @@ -395,7 +459,7 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec } public static Context getContext() { - return app; + return dashboardContext; } public static Provider getProvider() { return provider; } diff --git a/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java b/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java index 9171e816..943e877a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java +++ b/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java @@ -1,25 +1,46 @@ package se.leap.bitmaskclient; -import android.content.*; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; -import se.leap.bitmaskclient.eip.*; +import static se.leap.bitmaskclient.eip.Constants.IS_ALWAYS_ON; +import static se.leap.bitmaskclient.eip.Constants.RESTART_ON_BOOT; +import static se.leap.bitmaskclient.eip.Constants.VPN_CERTIFICATE; public class OnBootReceiver extends BroadcastReceiver { SharedPreferences preferences; - // Debug: am broadcast -a android.intent.action.BOOT_COMPLETED + + // Debug: su && am broadcast -a android.intent.action.BOOT_COMPLETED @Override public void onReceive(Context context, Intent intent) { preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, Context.MODE_PRIVATE); - boolean provider_configured = !preferences.getString(Provider.KEY, "").isEmpty(); - boolean start_on_boot = preferences.getBoolean(Dashboard.START_ON_BOOT, false); - if (provider_configured && start_on_boot) { - Intent dashboard_intent = new Intent(context, Dashboard.class); - dashboard_intent.setAction(Constants.ACTION_START_EIP); - dashboard_intent.putExtra(Dashboard.ON_BOOT, true); - dashboard_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(dashboard_intent); + boolean provider_configured = !preferences.getString(VPN_CERTIFICATE, "").isEmpty(); + boolean start_on_boot = preferences.getBoolean(RESTART_ON_BOOT, false); + boolean isAlwaysOnConfigured = preferences.getBoolean(IS_ALWAYS_ON, false); + Log.d("OpenVPN", "OpenVPN onBoot intent received. Provider configured? " + provider_configured + " Start on boot? " + start_on_boot + " isAlwaysOn feature configured: " + isAlwaysOnConfigured); + if (provider_configured) { + if (isAlwaysOnConfigured) { + //exit because the app is already setting up the vpn + return; + } + if (start_on_boot) { + Intent dashboard_intent = new Intent(context, Dashboard.class); + dashboard_intent.putExtra(RESTART_ON_BOOT, true); + dashboard_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(dashboard_intent); + } + } else { + if (isAlwaysOnConfigured) { + Intent dashboard_intent = new Intent(context, Dashboard.class); + dashboard_intent.putExtra(Dashboard.ACTION_CONFIGURE_ALWAYS_ON_PROFILE, true); + dashboard_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(dashboard_intent); + } } } } diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java b/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java index c85b0151..57c066aa 100644 --- a/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java @@ -16,24 +16,50 @@ */ package se.leap.bitmaskclient; -import android.app.*; -import android.content.*; -import android.os.*; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ResultReceiver; import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; -import org.jetbrains.annotations.*; +import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Observable; +import java.util.Observer; -import butterknife.*; +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; +import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.IOpenVPNServiceInternal; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.core.VpnStatus; -import mbanje.kurt.fabbutton.*; -import se.leap.bitmaskclient.eip.*; +import mbanje.kurt.fabbutton.FabButton; +import se.leap.bitmaskclient.eip.Constants; +import se.leap.bitmaskclient.eip.EIP; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.eip.VoidVpnService; + +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; +import static se.leap.bitmaskclient.eip.EipStatus.EipLevel.BLOCKING; +import static se.leap.bitmaskclient.eip.EipStatus.EipLevel.CONNECTED; +import static se.leap.bitmaskclient.eip.EipStatus.EipLevel.CONNECTING; +import static se.leap.bitmaskclient.eip.EipStatus.EipLevel.DISCONNECTED; +import static se.leap.bitmaskclient.eip.EipStatus.EipLevel.DISCONNECTING; public class VpnFragment extends Fragment implements Observer { @@ -41,7 +67,7 @@ public class VpnFragment extends Fragment implements Observer { public static final String IS_PENDING = TAG + ".is_pending"; protected static final String IS_CONNECTED = TAG + ".is_connected"; - public static final String START_ON_BOOT = "start on boot"; + public static final String START_EIP_ON_BOOT = "start on boot"; @InjectView(R.id.vpn_status_image) FabButton vpn_status_image; @@ -94,26 +120,19 @@ public class VpnFragment extends Fragment implements Observer { ButterKnife.inject(this, view); Bundle arguments = getArguments(); - if (arguments != null && arguments.containsKey(START_ON_BOOT) && arguments.getBoolean(START_ON_BOOT)) + if (arguments != null && arguments.containsKey(START_EIP_ON_BOOT) && arguments.getBoolean(START_EIP_ON_BOOT)) { startEipFromScratch(); - if (savedInstanceState != null) restoreState(savedInstanceState); + } return view; } - private void restoreState(@NotNull Bundle savedInstanceState) { - if (savedInstanceState.getBoolean(IS_PENDING)) - eip_status.setConnecting(); - else if (savedInstanceState.getBoolean(IS_CONNECTED)) - eip_status.setConnectedOrDisconnected(); - } - @Override public void onResume() { super.onResume(); //FIXME: avoid race conditions while checking certificate an logging in at about the same time //eipCommand(Constants.ACTION_CHECK_CERT_VALIDITY); - handleNewState(eip_status); + handleNewState(); bindOpenVpnService(); } @@ -130,9 +149,9 @@ public class VpnFragment extends Fragment implements Observer { super.onSaveInstanceState(outState); } - private void saveStatus() { - boolean is_on = eip_status.isConnected() || eip_status.isConnecting(); - Dashboard.preferences.edit().putBoolean(Dashboard.START_ON_BOOT, is_on).commit(); + private void saveStatus(boolean restartOnBoot) { + //boolean is_on = eip_status.isConnected() || eip_status.isConnecting() || eip_status.isBlocking(); + Dashboard.preferences.edit().putBoolean(Constants.RESTART_ON_BOOT, restartOnBoot).commit(); } @OnClick(R.id.vpn_main_button) @@ -142,7 +161,7 @@ public class VpnFragment extends Fragment implements Observer { else handleSwitchOn(); - saveStatus(); + saveStatus(eip_status.isConnected() || eip_status.isConnecting()); } private void handleSwitchOn() { @@ -177,8 +196,11 @@ public class VpnFragment extends Fragment implements Observer { askPendingStartCancellation(); } else if (eip_status.isConnected()) { askToStopEIP(); - } else + } else if (eip_status.isBlocking()) { + stop(); + } else { updateIcon(); + } } private void askPendingStartCancellation() { @@ -201,25 +223,26 @@ public class VpnFragment extends Fragment implements Observer { public void startEipFromScratch() { wants_to_connect = false; - eip_status.setConnecting(); + //eip_status.setEipLevel(BLOCKING); - saveStatus(); + saveStatus(true); eipCommand(Constants.ACTION_START_EIP); } private void stop() { - if (eip_status.isConnecting()) + + if (eip_status.isBlockingVpnEstablished()) { + Log.d(TAG, "stop VoidVpn!"); VoidVpnService.stop(); + } disconnect(); } private void disconnect() { - eip_status.setDisconnecting(); ProfileManager.setConntectedVpnProfileDisconnected(dashboard); if (mService != null) { try { mService.stopVPN(false); - eip_status.setConnectedOrDisconnected(); } catch (RemoteException e) { VpnStatus.logException(e); } @@ -227,6 +250,7 @@ public class VpnFragment extends Fragment implements Observer { } protected void stopEipIfPossible() { + //FIXME: no need to start a service here! eipCommand(Constants.ACTION_STOP_EIP); } @@ -276,36 +300,33 @@ public class VpnFragment extends Fragment implements Observer { public void update(Observable observable, Object data) { if (observable instanceof EipStatus) { eip_status = (EipStatus) observable; - final EipStatus eip_status = (EipStatus) observable; dashboard.runOnUiThread(new Runnable() { @Override public void run() { - handleNewState(eip_status); + handleNewState(); } }); } } - private void handleNewState(EipStatus eip_status) { - Context context = dashboard.getApplicationContext(); - String error = eip_status.lastError(5, context); - - if (!error.isEmpty()) VoidVpnService.stop(); + private void handleNewState() { updateIcon(); updateButton(); } private void updateIcon() { - if (eip_status.isConnected() || eip_status.isConnecting()) { - if(eip_status.isConnecting()) { - vpn_status_image.showProgress(true); - vpn_status_image.setIcon(R.drawable.ic_stat_vpn_empty_halo, R.drawable.ic_stat_vpn_empty_halo); - vpn_status_image.setTag(R.drawable.ic_stat_vpn_empty_halo); - } else { - vpn_status_image.showProgress(false); - vpn_status_image.setIcon(R.drawable.ic_stat_vpn, R.drawable.ic_stat_vpn); - vpn_status_image.setTag(R.drawable.ic_stat_vpn); - } + if (eip_status.isBlocking()) { + vpn_status_image.showProgress(false); + vpn_status_image.setIcon(R.drawable.ic_stat_vpn_blocking, R.drawable.ic_stat_vpn_blocking); + vpn_status_image.setTag(R.drawable.ic_stat_vpn_blocking); + } else if (eip_status.isConnecting()) { + vpn_status_image.showProgress(true); + vpn_status_image.setIcon(R.drawable.ic_stat_vpn_empty_halo, R.drawable.ic_stat_vpn_empty_halo); + vpn_status_image.setTag(R.drawable.ic_stat_vpn_empty_halo); + } else if (eip_status.isConnected()){ + vpn_status_image.showProgress(false); + vpn_status_image.setIcon(R.drawable.ic_stat_vpn, R.drawable.ic_stat_vpn); + vpn_status_image.setTag(R.drawable.ic_stat_vpn); } else { vpn_status_image.setIcon(R.drawable.ic_stat_vpn_offline, R.drawable.ic_stat_vpn_offline); vpn_status_image.setTag(R.drawable.ic_stat_vpn_offline); @@ -314,17 +335,28 @@ public class VpnFragment extends Fragment implements Observer { } private void updateButton() { - if (eip_status.isConnected() || eip_status.isConnecting()) { - if(eip_status.isConnecting()) { - main_button.setText(dashboard.getString(android.R.string.cancel)); - } else { - main_button.setText(dashboard.getString(R.string.vpn_button_turn_off)); - } + if (eip_status.isConnecting()) { + main_button.setText(dashboard.getString(android.R.string.cancel)); + } else if (eip_status.isConnected() || isOpenVpnRunningWithoutNetwork()) { + main_button.setText(dashboard.getString(R.string.vpn_button_turn_off)); } else { main_button.setText(dashboard.getString(R.string.vpn_button_turn_on)); } } + private boolean isOpenVpnRunningWithoutNetwork() { + boolean isRunning = false; + try { + isRunning = eip_status.getLevel() == LEVEL_NONETWORK && + mService.isVpnRunning(); + } catch (Exception e) { + //eat me + e.printStackTrace(); + } + + return isRunning; + } + private void bindOpenVpnService() { Intent intent = new Intent(dashboard, OpenVPNService.class); intent.setAction(OpenVPNService.START_SERVICE); @@ -381,7 +413,7 @@ public class VpnFragment extends Fragment implements Observer { startEipFromScratch(); break; case Activity.RESULT_CANCELED: - handleNewState(eip_status); + handleNewState(); break; } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java b/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java index db1cb4a1..ed4ebcbc 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java @@ -27,6 +27,7 @@ public interface Constants { public final static String ACTION_CHECK_CERT_VALIDITY = TAG + ".CHECK_CERT_VALIDITY"; public final static String ACTION_START_EIP = TAG + ".START_EIP"; + public final static String ACTION_START_ALWAYS_ON_EIP = TAG + ".START_ALWAYS_ON_EIP"; public final static String ACTION_STOP_EIP = TAG + ".STOP_EIP"; public final static String ACTION_UPDATE_EIP_SERVICE = TAG + ".UPDATE_EIP_SERVICE"; public final static String ACTION_IS_EIP_RUNNING = TAG + ".IS_RUNNING"; @@ -40,5 +41,7 @@ public interface Constants { public final static String REQUEST_TAG = TAG + ".REQUEST_TAG"; public final static String START_BLOCKING_VPN_PROFILE = TAG + ".START_BLOCKING_VPN_PROFILE"; public final static String PROVIDER_CONFIGURED = TAG + ".PROVIDER_CONFIGURED"; + public final static String IS_ALWAYS_ON = TAG + ".IS_ALWAYS_ON"; + public final static String RESTART_ON_BOOT = TAG + ".RESTART_ON_BOOT"; } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java index 28a9bb50..39dd133f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -19,6 +19,7 @@ package se.leap.bitmaskclient.eip; import android.app.*; import android.content.*; import android.os.*; +import android.util.Log; import org.json.*; @@ -57,7 +58,6 @@ public final class EIP extends IntentService { @Override public void onCreate() { super.onCreate(); - context = getApplicationContext(); preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); eip_definition = eipDefinitionFromPreferences(); @@ -72,6 +72,8 @@ public final class EIP extends IntentService { if (action.equals(ACTION_START_EIP)) startEIP(); + else if (action.equals(ACTION_START_ALWAYS_ON_EIP)) + startAlwaysOnEIP(); else if (action.equals(ACTION_STOP_EIP)) stopEIP(); else if (action.equals(ACTION_IS_EIP_RUNNING)) @@ -88,9 +90,12 @@ public final class EIP extends IntentService { * It also sets up early routes. */ private void startEIP() { + Log.d(TAG, "startEIP vpn"); if (gateways_manager.isEmpty()) updateEIPService(); - earlyRoutes(); + if (!EipStatus.getInstance().isBlockingVpnEstablished()) { + earlyRoutes(); + } gateway = gateways_manager.select(); if (gateway != null && gateway.getProfile() != null) { @@ -101,6 +106,27 @@ public final class EIP extends IntentService { tellToReceiver(ACTION_START_EIP, Activity.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 startAlwaysOnEIP() { + Log.d(TAG, "startAlwaysOnEIP vpn"); + + if (gateways_manager.isEmpty()) + updateEIPService(); + + gateway = gateways_manager.select(); + + if (gateway != null && gateway.getProfile() != null) { + //mReceiver = VpnFragment.getReceiver(); + Log.d(TAG, "startAlwaysOnEIP eip launch avtive gateway vpn"); + launchActiveGateway(); + } else { + Log.d(TAG, "startAlwaysOnEIP no active profile available!"); + } + } + /** * Early routes are routes that block traffic until a new * VpnService is started properly. diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java index 501543b8..dc2e81f5 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java @@ -17,19 +17,37 @@ package se.leap.bitmaskclient.eip; import android.content.*; +import android.os.AsyncTask; +import android.support.annotation.VisibleForTesting; import java.util.*; import de.blinkt.openvpn.core.*; +/** + * EipStatus is a Singleton that represents a reduced set of a vpn's ConnectionStatus. + * EipStatus changes it's state (EipLevel) when ConnectionStatus gets updated by OpenVpnService or + * by VoidVpnService. + */ public class EipStatus extends Observable implements VpnStatus.StateListener { public static String TAG = EipStatus.class.getSimpleName(); private static EipStatus current_status; + public enum EipLevel { + CONNECTING, + DISCONNECTING, + CONNECTED, + DISCONNECTED, + BLOCKING, + UNKNOWN + } - private static ConnectionStatus level = ConnectionStatus.LEVEL_NOTCONNECTED; - private static boolean - wants_to_disconnect = false, - is_connecting = false; + /** + * vpn_level holds the connection status of the openvpn vpn and the traffic blocking + * void vpn. LEVEL_BLOCKING is set when the latter vpn is up. All other states are set by + * openvpn. + */ + private ConnectionStatus vpn_level = ConnectionStatus.LEVEL_NOTCONNECTED; + private EipLevel current_eip_level = EipLevel.DISCONNECTED; int last_error_line = 0; private String state, log_message; @@ -48,64 +66,137 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { @Override public void updateState(final String state, final String logmessage, final int localizedResId, final ConnectionStatus level) { - updateStatus(state, logmessage, localizedResId, level); - if (isConnected() || isDisconnected() || wantsToDisconnect()) { - setConnectedOrDisconnected(); - } else - setConnecting(); + current_status = getInstance(); + current_status.setState(state); + current_status.setLogMessage(logmessage); + current_status.setLocalizedResId(localizedResId); + current_status.setLevel(level); + current_status.setEipLevel(level); } @Override public void setConnectedVPN(String uuid) { } - private void updateStatus(final String state, final String logmessage, final int localizedResId, final ConnectionStatus level) { - current_status = getInstance(); - current_status.setState(state); - current_status.setLogMessage(logmessage); - current_status.setLocalizedResId(localizedResId); - current_status.setLevel(level); - current_status.setChanged(); + + private void setEipLevel(ConnectionStatus level) { + EipLevel tmp = current_eip_level; + switch (level) { + case LEVEL_CONNECTED: + current_eip_level = EipLevel.CONNECTED; + break; + case LEVEL_VPNPAUSED: + throw new IllegalStateException("Ics-Openvpn's VPNPAUSED state is not supported by Bitmask"); + case LEVEL_CONNECTING_SERVER_REPLIED: + case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: + case LEVEL_WAITING_FOR_USER_INPUT: + case LEVEL_START: + current_eip_level = EipLevel.CONNECTING; + break; + case LEVEL_AUTH_FAILED: + case LEVEL_NOTCONNECTED: + current_eip_level = EipLevel.DISCONNECTED; + break; + case LEVEL_NONETWORK: + case LEVEL_BLOCKING: + setEipLevelWithDelay(level); + break; + case UNKNOWN_LEVEL: + current_eip_level = EipLevel.UNKNOWN; //?? + break; + } + if (tmp != current_eip_level) { + current_status.setChanged(); + current_status.notifyObservers(); + } } - public boolean wantsToDisconnect() { - return wants_to_disconnect; + @VisibleForTesting + EipLevel getEipLevel() { + return current_eip_level; } - public boolean isConnecting() { - return is_connecting; + /** + * This method intends to ignore states that are valid for less than a second. + * This way flickering UI changes can be avoided + * + * @param futureLevel + */ + private void setEipLevelWithDelay(ConnectionStatus futureLevel) { + new DelayTask(current_status.getLevel(), futureLevel).execute(); } - public boolean isConnected() { - return level == ConnectionStatus.LEVEL_CONNECTED; + private class DelayTask extends AsyncTask { + + private final ConnectionStatus currentLevel; + private final ConnectionStatus futureLevel; + + public DelayTask(ConnectionStatus currentLevel, ConnectionStatus futureLevel) { + this.currentLevel = currentLevel; + this.futureLevel = futureLevel; + } + protected Void doInBackground(Void... levels) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.interrupted(); + } + return null; + } + + protected void onPostExecute(Void result) {; + if (currentLevel == current_status.getLevel()) { + switch (futureLevel) { + case LEVEL_NONETWORK: + current_eip_level = EipLevel.DISCONNECTED; + break; + case LEVEL_BLOCKING: + current_eip_level = EipLevel.BLOCKING; + break; + default: + break; + } + current_status.setChanged(); + current_status.notifyObservers(); + } + } } - public boolean isDisconnected() { - return level == ConnectionStatus.LEVEL_NOTCONNECTED; + public boolean isConnecting() { + return current_eip_level == EipLevel.CONNECTING; } - public boolean isPaused() { - return level == ConnectionStatus.LEVEL_VPNPAUSED; + public boolean isConnected() { + return current_eip_level == EipLevel.CONNECTED; } - public void setConnecting() { - is_connecting = true; + /** + * @return true if current_eip_level is for at least a second {@link EipLevel#BLOCKING}. + * See {@link #setEipLevelWithDelay(ConnectionStatus)}. + */ + public boolean isBlocking() { + return current_eip_level == EipLevel.BLOCKING; + } - wants_to_disconnect = false; - current_status.setChanged(); - current_status.notifyObservers(); + /** + * + * @return true immediately after traffic blocking VoidVpn was established. + */ + public boolean isBlockingVpnEstablished() { + return vpn_level == ConnectionStatus.LEVEL_BLOCKING; } - public void setConnectedOrDisconnected() { - is_connecting = false; - wants_to_disconnect = false; - current_status.setChanged(); - current_status.notifyObservers(); + public boolean isDisconnected() { + return current_eip_level == EipLevel.DISCONNECTED; } - public void setDisconnecting() { - wants_to_disconnect = true; - is_connecting = false; + /** + * ics-openvpn's paused state is not implemented yet + * @return + */ + @Deprecated + public boolean isPaused() { + return vpn_level == ConnectionStatus.LEVEL_VPNPAUSED; } public String getState() { @@ -121,7 +212,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { } public ConnectionStatus getLevel() { - return level; + return vpn_level; } private void setState(String state) { @@ -137,7 +228,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { } private void setLevel(ConnectionStatus level) { - EipStatus.level = level; + this.vpn_level = level; } public boolean errorInLast(int lines, Context context) { @@ -169,7 +260,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { @Override public String toString() { - return "State: " + state + " Level: " + level.toString(); + return "State: " + state + " Level: " + vpn_level.toString(); } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java index cbf0fed2..46c010ca 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java @@ -1,41 +1,58 @@ package se.leap.bitmaskclient.eip; -import android.content.*; -import android.net.*; -import android.os.*; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.VpnService; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; + +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.Dashboard.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.eip.Constants.ACTION_START_ALWAYS_ON_EIP; -import java.io.*; public class VoidVpnService extends VpnService { static final String TAG = VoidVpnService.class.getSimpleName(); static ParcelFileDescriptor fd; - static Thread thread; + private final int ALWAYS_ON_MIN_API_LEVEL = Build.VERSION_CODES.N; + private static final String STATE_ESTABLISH = "ESTABLISHVOIDVPN"; + private static final String STATE_STOP = "STOPVOIDVPN"; @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(final Intent intent, int flags, int startId) { String action = intent != null ? intent.getAction() : ""; - if (action == Constants.START_BLOCKING_VPN_PROFILE) { + if (action.equals(Constants.START_BLOCKING_VPN_PROFILE)) { thread = new Thread(new Runnable() { public void run() { - Builder builder = new Builder(); - builder.setSession("Blocking until running"); - builder.addAddress("10.42.0.8", 16); - builder.addRoute("0.0.0.0", 1); - builder.addRoute("192.168.1.0", 24); - builder.addDnsServer("10.42.0.1"); - try { - fd = builder.establish(); - - } catch (Exception e) { - e.printStackTrace(); - } + establishBlockingVpn(); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putBoolean(Constants.IS_ALWAYS_ON, false).commit(); + Log.d(TAG, "start blocking vpn profile - always on = false"); + } + }); + thread.run(); + } else if (action.equals("android.net.VpnService") && Build.VERSION.SDK_INT >= ALWAYS_ON_MIN_API_LEVEL) { + //only always-on feature triggers this + thread = new Thread(new Runnable() { + public void run() { + establishBlockingVpn(); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putBoolean(Constants.IS_ALWAYS_ON, true).commit(); + requestVpnWithLastSelectedProfile(); + Log.d(TAG, "start blocking vpn profile - always on = true"); } }); thread.run(); } - return 0; + return START_STICKY; } @Override @@ -48,6 +65,8 @@ public class VoidVpnService extends VpnService { if (thread != null) thread.interrupt(); closeFd(); + VpnStatus.updateStateString(STATE_STOP, "", + R.string.void_vpn_stopped, ConnectionStatus.LEVEL_NOTCONNECTED); } public static boolean isRunning() throws NullPointerException { @@ -62,4 +81,45 @@ public class VoidVpnService extends VpnService { e.printStackTrace(); } } + + private Builder prepareBlockingVpnProfile() { + Builder builder = new Builder(); + builder.setSession("Blocking until running"); + builder.addRoute("0.0.0.0", 1); + builder.addRoute("192.168.1.0", 24); + builder.addDnsServer("10.42.0.1"); + builder.addAddress("10.42.0.8", 16); + return builder; + + } + + private void establishBlockingVpn() { + try { + VpnStatus.logInfo(getString(R.string.void_vpn_establish)); + VpnStatus.updateStateString(STATE_ESTABLISH, "", + R.string.void_vpn_establish, ConnectionStatus.LEVEL_BLOCKING); + Builder builder = prepareBlockingVpnProfile(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.addDisallowedApplication(getPackageName()); + } + + fd = builder.establish(); + } catch (Exception e) { + // Catch any exception + e.printStackTrace(); + VpnStatus.logError(R.string.void_vpn_error_establish); + } + } + + private void requestVpnWithLastSelectedProfile() { + Intent startEIP = new Intent(getApplicationContext(), EIP.class); + startEIP.setAction(ACTION_START_ALWAYS_ON_EIP); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //noinspection NewApi + getApplicationContext().startForegroundService(startEIP); + } else { + getApplicationContext().startService(startEIP); + } + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java index d124c395..37744927 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java @@ -159,7 +159,7 @@ public class SessionDialog extends DialogFragment { public void logIn(String username, String password); public void signUp(String username, String password); - + //FIXME: can we remove this method? public void cancelLoginOrSignup(); } diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java index 20189904..14323f8e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java @@ -24,7 +24,6 @@ import se.leap.bitmaskclient.ProviderAPI; import se.leap.bitmaskclient.ProviderAPICommand; import se.leap.bitmaskclient.ProviderAPIResultReceiver; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.eip.EipStatus; public class UserStatusFragment extends Fragment implements Observer, SessionDialog.SessionDialogInterface { @@ -169,7 +168,7 @@ public class UserStatusFragment extends Fragment implements Observer, SessionDia } public void cancelLoginOrSignup() { - EipStatus.getInstance().setConnectedOrDisconnected(); + //EipStatus.getInstance().setConnectedOrDisconnected(); } private Bundle bundlePassword(String password) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04e83a18..70f6f4ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,5 +76,10 @@ is being registered. Turn on Turn off + Stop blocking Bitmask Log + Bitmask blocks all outgoing internet traffic. + Failed to establish blocking VPN. + Stopped blocking all outgoing internet traffic. + Blocking traffic -- cgit v1.2.3