diff options
author | cyberta <cyberta@riseup.net> | 2020-01-31 22:46:24 -0800 |
---|---|---|
committer | cyberta <cyberta@riseup.net> | 2020-01-31 22:46:24 -0800 |
commit | 0e8f40e75eb1a5fe2d3c212b5939fdbf427ec0f5 (patch) | |
tree | f6b0adef18755cc8c107897e625595614e5dce36 | |
parent | 721d222a457ec0dfec28bc4ee4908b50f04904fc (diff) | |
parent | b8ba423d997f5dbb2541b4f4542a2b6b30400485 (diff) |
Merge branch 'implement_tethering_for_rooted_devices' into 'master'
Implement tethering for rooted devices
See merge request leap/bitmask_android!98
65 files changed, 2082 insertions, 243 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9e8c0d98..83394209 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> 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 766dc925..67d3c4f2 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -47,7 +47,7 @@ import de.blinkt.openvpn.core.connection.Obfs4Connection; import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.VpnNotificationManager; import se.leap.bitmaskclient.pluggableTransports.Shapeshifter; -import se.leap.bitmaskclient.utils.FirewallHelper; +import se.leap.bitmaskclient.firewall.FirewallManager; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; @@ -90,7 +90,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac private Runnable mOpenVPNThread; private VpnNotificationManager notificationManager; private Shapeshifter shapeshifter; - private FirewallHelper firewallHelper; + private FirewallManager firewallManager; private static final int PRIORITY_MIN = -2; private static final int PRIORITY_DEFAULT = 0; @@ -194,7 +194,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac VpnStatus.removeStateListener(this); } } - firewallHelper.shutdownFirewall(); + firewallManager.stop(); } private boolean runningOnAndroidTV() { @@ -309,7 +309,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac return START_REDELIVER_INTENT; } - /* TODO: check that for Bitmask */ + /* TODO: check that for Bitmask */ // Always show notification here to avoid problem with startForeground timeout VpnStatus.logInfo(R.string.building_configration); VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START); @@ -449,14 +449,12 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac mProcessThread.start(); } - firewallHelper.startFirewall(); - new Handler(getMainLooper()).post(() -> { - if (mDeviceStateReceiver != null) { - unregisterDeviceStateReceiver(); - } - registerDeviceStateReceiver(mManagement); - } + if (mDeviceStateReceiver != null) { + unregisterDeviceStateReceiver(); + } + registerDeviceStateReceiver(mManagement); + } ); } @@ -518,7 +516,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac super.onCreate(); notificationManager = new VpnNotificationManager(this, this); notificationManager.createOpenVpnNotificationChannel(); - firewallHelper = new FirewallHelper(this); + firewallManager = new FirewallManager(this, true); } @Override @@ -537,6 +535,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac VpnStatus.flushLog(); notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_BG_ID); notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_NEWSTATUS_ID); + firewallManager.onDestroy(); } private String getTunConfigString() { @@ -967,30 +966,25 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac String channel = NOTIFICATION_CHANNEL_NEWSTATUS_ID; // Display byte count only after being connected - { - if (level == LEVEL_CONNECTED) { - mDisplayBytecount = true; - mConnecttime = System.currentTimeMillis(); - if (!runningOnAndroidTV()) - channel = NOTIFICATION_CHANNEL_BG_ID; - } else { - mDisplayBytecount = false; - } + if (level == LEVEL_CONNECTED) { + mDisplayBytecount = true; + mConnecttime = System.currentTimeMillis(); + if (!runningOnAndroidTV()) + channel = NOTIFICATION_CHANNEL_BG_ID; + firewallManager.start(); + } else { + mDisplayBytecount = false; + } - // Other notifications are shown, - // This also mean we are no longer connected, ignore bytecount messages until next - // CONNECTED - // Does not work :( - notificationManager.buildOpenVpnNotification( - mProfile != null ? mProfile.mName : "", - mProfile != null && mProfile.mUsePluggableTransports, - VpnStatus.getLastCleanLogMessage(this), - VpnStatus.getLastCleanLogMessage(this), - level, - 0, - channel); + notificationManager.buildOpenVpnNotification( + mProfile != null ? mProfile.mName : "", + mProfile != null && mProfile.mUsePluggableTransports, + VpnStatus.getLastCleanLogMessage(this), + VpnStatus.getLastCleanLogMessage(this), + level, + 0, + channel); - } } @Override diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java index 45664653..48910fb5 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java +++ b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java @@ -8,6 +8,8 @@ import android.support.v7.app.AppCompatDelegate; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; +import se.leap.bitmaskclient.tethering.TetheringStateManager; + import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; import static se.leap.bitmaskclient.utils.PreferenceHelper.getSavedProviderFromSharedPreferences; @@ -38,6 +40,7 @@ public class BitmaskApp extends MultiDexApplication { providerObservable.updateProvider(getSavedProviderFromSharedPreferences(preferences)); EipSetupObserver.init(this, preferences); AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + TetheringStateManager.getInstance().init(this); } /** diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java index 5ed28806..60edc941 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java @@ -16,6 +16,11 @@ public interface Constants { String EXCLUDED_APPS = "excluded_apps"; String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports"; String SU_PERMISSION = "su_permission"; + String ALLOW_TETHERING_BLUETOOTH = "tethering_bluetooth"; + String ALLOW_TETHERING_WIFI = "tethering_wifi"; + String ALLOW_TETHERING_USB = "tethering_usb"; + String SHOW_EXPERIMENTAL = "show_experimental"; + String USE_IPv6_FIREWALL = "use_ipv6_firewall"; ////////////////////////////////////////////// @@ -55,6 +60,7 @@ public interface Constants { String EIP_ACTION_START_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_START_BLOCKING_VPN"; String EIP_ACTION_STOP_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_STOP_BLOCKING_VPN"; String EIP_ACTION_PREPARE_VPN = "se.leap.bitmaskclient.EIP_ACTION_PREPARE_VPN"; + String EIP_ACTION_CONFIGURE_TETHERING = "se.leap.bitmaskclient.EIP_ACTION_CONFIGURE_TETHERING"; String EIP_RECEIVER = "EIP.RECEIVER"; String EIP_REQUEST = "EIP.REQUEST"; diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java index 74f132b9..84516ee3 100644 --- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java +++ b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java @@ -44,7 +44,6 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE; import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE; import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK; /** * Created by cyberta on 05.12.18. diff --git a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java index 3144d62c..104f1edc 100644 --- a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java @@ -43,6 +43,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import java.util.Set; @@ -56,10 +57,12 @@ import se.leap.bitmaskclient.ProviderListActivity; import se.leap.bitmaskclient.ProviderObservable; import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.firewall.FirewallManager; import se.leap.bitmaskclient.fragments.AboutFragment; import se.leap.bitmaskclient.fragments.AlwaysOnDialog; import se.leap.bitmaskclient.fragments.ExcludeAppsFragment; import se.leap.bitmaskclient.fragments.LogFragment; +import se.leap.bitmaskclient.fragments.TetheringDialog; import se.leap.bitmaskclient.utils.PreferenceHelper; import se.leap.bitmaskclient.views.IconSwitchEntry; import se.leap.bitmaskclient.views.IconTextEntry; @@ -81,6 +84,7 @@ import static se.leap.bitmaskclient.utils.PreferenceHelper.getSaveBattery; import static se.leap.bitmaskclient.utils.PreferenceHelper.getShowAlwaysOnDialog; import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; import static se.leap.bitmaskclient.utils.PreferenceHelper.saveBattery; +import static se.leap.bitmaskclient.utils.PreferenceHelper.showExperimentalFeatures; import static se.leap.bitmaskclient.utils.PreferenceHelper.usePluggableTransports; /** @@ -109,6 +113,9 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen private Toolbar toolbar; private IconTextEntry account; private IconSwitchEntry saveBattery; + private IconTextEntry tethering; + private IconSwitchEntry firewall; + private View experimentalFeatureFooter; private boolean userLearnedDrawer; private volatile boolean wasPaused; @@ -117,7 +124,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen private SharedPreferences preferences; private final static String KEY_SHOW_SAVE_BATTERY_ALERT = "KEY_SHOW_SAVE_BATTERY_ALERT"; - private boolean showEnableExperimentalFeature = false; + private volatile boolean showSaveBattery = false; AlertDialog alertDialog; @Override @@ -237,6 +244,10 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen initSaveBatteryEntry(); initAlwaysOnVpnEntry(); initExcludeAppsEntry(); + initShowExperimentalHint(); + initTetheringEntry(); + initFirewallEntry(); + initExperimentalFeatureFooter(); initDonateEntry(); initLogEntry(); initAboutEntry(); @@ -339,6 +350,62 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen } } + private void initShowExperimentalHint() { + TextView textView = drawerLayout.findViewById(R.id.show_experimental_features); + textView.setText(showExperimentalFeatures(getContext()) ? R.string.hide_experimental : R.string.show_experimental); + textView.setOnClickListener(v -> { + boolean shown = showExperimentalFeatures(getContext()); + if (shown) { + tethering.setVisibility(GONE); + firewall.setVisibility(GONE); + experimentalFeatureFooter.setVisibility(GONE); + ((TextView) v).setText(R.string.show_experimental); + } else { + tethering.setVisibility(VISIBLE); + firewall.setVisibility(VISIBLE); + experimentalFeatureFooter.setVisibility(VISIBLE); + ((TextView) v).setText(R.string.hide_experimental); + } + PreferenceHelper.setShowExperimentalFeatures(getContext(), !shown); + }); + } + + private void initFirewallEntry() { + firewall = drawerView.findViewById(R.id.enableIPv6Firewall); + boolean show = showExperimentalFeatures(getContext()); + firewall.setVisibility(show ? VISIBLE : GONE); + firewall.setChecked(PreferenceHelper.useIpv6Firewall(this.getContext())); + firewall.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + PreferenceHelper.setUseIPv6Firewall(getContext(), isChecked); + FirewallManager firewallManager = new FirewallManager(getContext().getApplicationContext(), false); + if (VpnStatus.isVPNActive()) { + if (isChecked) { + firewallManager.startIPv6Firewall(); + } else { + firewallManager.stopIPv6Firewall(); + } + } + }); + } + + private void initTetheringEntry() { + tethering = drawerView.findViewById(R.id.tethering); + boolean show = showExperimentalFeatures(getContext()); + tethering.setVisibility(show ? VISIBLE : GONE); + tethering.setOnClickListener((buttonView) -> { + showTetheringAlert(); + }); + } + + private void initExperimentalFeatureFooter() { + experimentalFeatureFooter = drawerView.findViewById(R.id.experimental_features_footer); + boolean show = showExperimentalFeatures(getContext()); + experimentalFeatureFooter.setVisibility(show ? VISIBLE : GONE); + } + private void initDonateEntry() { if (ENABLE_DONATION) { IconTextEntry donate = drawerView.findViewById(R.id.donate); @@ -415,8 +482,9 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - if (showEnableExperimentalFeature) { + if (showSaveBattery) { outState.putBoolean(KEY_SHOW_SAVE_BATTERY_ALERT, true); + alertDialog.dismiss(); } } @@ -434,7 +502,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen try { AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); - showEnableExperimentalFeature = true; + showSaveBattery = true; alertDialog = alertBuilder .setTitle(activity.getString(R.string.save_battery)) .setMessage(activity.getString(R.string.save_battery_message)) @@ -442,13 +510,26 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen saveBattery(getContext(), true); }) .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false)) - .setOnDismissListener(dialog -> showEnableExperimentalFeature = false) + .setOnDismissListener(dialog -> showSaveBattery = false) .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show(); } catch (IllegalStateException e) { e.printStackTrace(); } } + public void showTetheringAlert() { + try { + + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + TetheringDialog.TAG); + DialogFragment newFragment = new TetheringDialog(); + newFragment.show(fragmentTransaction, TetheringDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } + public void showAlwaysOnDialog() { try { 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 1d67cd12..1186b54f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -61,6 +61,7 @@ import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; 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_CONFIGURE_TETHERING; 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; @@ -190,6 +191,9 @@ public final class EIP extends JobIntentService implements Observer { disconnect(); earlyRoutes(); break; + case EIP_ACTION_CONFIGURE_TETHERING: + Log.d(TAG, "TODO: implement tethering configuration"); + break; } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java index eb266e3c..d2667e42 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import se.leap.bitmaskclient.Provider; import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_CONFIGURE_TETHERING; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP; @@ -90,4 +92,13 @@ public class EipCommand { execute(context, EIP_ACTION_CHECK_CERT_VALIDITY, resultReceiver, null); } + public static void configureTethering(@NonNull Context context) { + execute(context, EIP_ACTION_CONFIGURE_TETHERING); + } + + @VisibleForTesting + public static void configureTethering(@NonNull Context context, ResultReceiver resultReceiver) { + execute(context, EIP_ACTION_CONFIGURE_TETHERING); + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallCallback.java b/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallCallback.java new file mode 100644 index 00000000..15fa426f --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallCallback.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2019 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.firewall; + +interface FirewallCallback { + void onFirewallStarted(boolean success); + void onFirewallStopped(boolean success); + void onTetheringStarted(boolean success); + void onTetheringStopped(boolean success); + void onSuRequested(boolean success); +} diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java b/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java new file mode 100644 index 00000000..c148497b --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java @@ -0,0 +1,151 @@ +package se.leap.bitmaskclient.firewall; +/** + * Copyright (c) 2019 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 <http://www.gnu.org/licenses/>. + */ + +import android.content.Context; + +import java.util.Observable; +import java.util.Observer; + +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.tethering.TetheringState; +import se.leap.bitmaskclient.utils.PreferenceHelper; + +public class FirewallManager implements FirewallCallback, Observer { + public static String BITMASK_CHAIN = "bitmask_fw"; + public static String BITMASK_FORWARD = "bitmask_forward"; + public static String BITMASK_POSTROUTING = "bitmask_postrouting"; + static final String TAG = FirewallManager.class.getSimpleName(); + private boolean isRunning = false; + + private Context context; + + public FirewallManager(Context context, boolean observeTethering) { + this.context = context; + if (observeTethering) { + TetheringObservable.getInstance().addObserver(this); + } + } + + @Override + public void onFirewallStarted(boolean success) { + if (success) { + VpnStatus.logInfo("[FIREWALL] Custom rules established"); + } else { + VpnStatus.logError("[FIREWALL] Could not establish custom rules."); + } + } + + @Override + public void onFirewallStopped(boolean success) { + if (success) { + VpnStatus.logInfo("[FIREWALL] Custom rules deleted"); + } else { + VpnStatus.logError("[FIREWALL] Could not delete custom rules"); + } + } + + @Override + public void onTetheringStarted(boolean success) { + if (success) { + VpnStatus.logInfo("[FIREWALL] Rules for tethering enabled"); + } else { + VpnStatus.logError("[FIREWALL] Could not enable rules for tethering."); + } + } + + @Override + public void onTetheringStopped(boolean success) { + if (success) { + VpnStatus.logInfo("[FIREWALL] Rules for tethering successfully disabled"); + } else { + VpnStatus.logError("[FIREWALL] Could not disable rules for tethering."); + } + } + + @Override + public void onSuRequested(boolean success) { + PreferenceHelper.setSuPermission(context, success); + if (!success) { + VpnStatus.logError("[FIREWALL] Root permission needed to execute custom firewall rules."); + } + } + + public void onDestroy() { + TetheringObservable.getInstance().deleteObserver(this); + } + + + public void start() { + if (!isRunning) { + isRunning = true; + if (PreferenceHelper.useIpv6Firewall(context)) { + startIPv6Firewall(); + } + TetheringState tetheringState = TetheringObservable.getInstance().getTetheringState(); + if (tetheringState.hasAnyDeviceTetheringEnabled() && tetheringState.hasAnyVpnTetheringAllowed()) { + startTethering(); + } + } + + } + + public void stop() { + isRunning = false; + if (PreferenceHelper.useIpv6Firewall(context)) { + stopIPv6Firewall(); + } + TetheringState tetheringState = TetheringObservable.getInstance().getTetheringState(); + if (tetheringState.hasAnyDeviceTetheringEnabled() && tetheringState.hasAnyVpnTetheringAllowed()) { + stopTethering(); + } + } + + public void startTethering() { + SetupTetheringTask task = new SetupTetheringTask(this); + task.execute(); + } + + public void stopTethering() { + ShutdownTetheringTask task = new ShutdownTetheringTask(this); + task.execute(); + } + + public void startIPv6Firewall() { + StartIPv6FirewallTask task = new StartIPv6FirewallTask(this); + task.execute(); + } + + public void stopIPv6Firewall() { + ShutdownIPv6FirewallTask task = new ShutdownIPv6FirewallTask(this); + task.execute(); + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TetheringObservable) { + TetheringObservable observable = (TetheringObservable) o; + TetheringState state = observable.getTetheringState(); + if (state.hasAnyVpnTetheringAllowed() && state.hasAnyDeviceTetheringEnabled()) { + startTethering(); + } else { + stopTethering(); + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java new file mode 100644 index 00000000..7abd01a8 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.firewall; + +import android.os.AsyncTask; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.net.NetworkInterface; +import java.util.ArrayList; +import java.util.Enumeration; + +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.tethering.TetheringState; + +import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_FORWARD; +import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_POSTROUTING; +import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; + +public class SetupTetheringTask extends AsyncTask<Void, Boolean, Boolean> { + + private static final String TAG = SetupTetheringTask.class.getSimpleName(); + private WeakReference<FirewallCallback> callbackWeakReference; + + SetupTetheringTask(FirewallCallback callback) { + callbackWeakReference = new WeakReference<>(callback); + } + + @Override + protected Boolean doInBackground(Void... args) { + TetheringState tetheringState = TetheringObservable.getInstance().getTetheringState(); + StringBuilder log = new StringBuilder(); + + String[] bitmaskChain = new String[]{ + "su", + "id", + "iptables -t filter --list " + BITMASK_FORWARD + " && iptables -t nat --list " + BITMASK_POSTROUTING }; + + try { + boolean hasBitmaskChain = runBlockingCmd(bitmaskChain, log) == 0; + boolean allowSu = log.toString().contains("uid=0"); + FirewallCallback callback = callbackWeakReference.get(); + if (callback != null) { + callback.onSuRequested(allowSu); + } + if (!allowSu) { + return false; + } + + boolean success = true; + log = new StringBuilder(); + + if (!hasBitmaskChain && tetheringState.hasAnyVpnTetheringAllowed() && tetheringState.hasAnyDeviceTetheringEnabled()) { + createChains(log); + } + + if (tetheringState.tetherWifiVpn()) { + log = new StringBuilder(); + success = addWifiTetheringRules(tetheringState, log); + logError(success, log); + } else if (!tetheringState.isVpnWifiTetheringAllowed){ + success = removeWifiTetheringRules(tetheringState, log); + logError(success, log); + } + + log = new StringBuilder(); + if (tetheringState.tetherUsbVpn()) { + success = success && addUsbTetheringRules(tetheringState, log); + logError(success, log); + } else if (!tetheringState.isVpnUsbTetheringAllowed) { + success = success && removeUsbTetheringRules(tetheringState, log); + logError(success, log); + } + + log = new StringBuilder(); + if (tetheringState.tetherBluetoothVpn()) { + success = success && addBluetoothTetheringRules(tetheringState, log); + logError(success, log); + } else if (!tetheringState.isVpnBluetoothTetheringAllowed) { + success = success && removeBluetoothTetheringRules(tetheringState, log); + logError(success, log); + } + return success; + } catch (Exception e) { + e.printStackTrace(); + Log.e(FirewallManager.TAG, log.toString()); + } + return false; + } + + private void logError(boolean success, StringBuilder log) { + if (!success) { + Log.e(TAG, log.toString()); + } + } + + + private void createChains(StringBuilder log) throws Exception { + boolean success; + String[] createChains = new String[]{ + "su", + "iptables -t filter --new-chain " + BITMASK_FORWARD, + "iptables -t nat --new-chain " + BITMASK_POSTROUTING, + "iptables -t filter --insert FORWARD --jump " + BITMASK_FORWARD, + "iptables -t nat --insert POSTROUTING --jump " + BITMASK_POSTROUTING, + }; + success = runBlockingCmd(createChains, log) == 0; + Log.d(FirewallManager.TAG, "added " + BITMASK_FORWARD + " and " + BITMASK_POSTROUTING+" to iptables: " + success); + Log.d(FirewallManager.TAG, log.toString()); + } + + private boolean addWifiTetheringRules(TetheringState state, StringBuilder log) throws Exception { + Log.d(TAG, "add Wifi tethering Rules"); + String[] addRules = getAdditionRules(state.wifiAddress, state.wifiInterface); + return runBlockingCmd(addRules, log) == 0; + } + + private boolean removeWifiTetheringRules(TetheringState state, StringBuilder log) throws Exception { + Log.d(TAG, "add Wifi tethering Rules"); + String[] removeRules = getDeletionRules(state, state.lastSeenWifiAddress, state.lastSeenWifiInterface); + return runBlockingCmd(removeRules, log) == 0; + } + + private boolean addUsbTetheringRules(TetheringState state, StringBuilder log) throws Exception { + Log.d(TAG, "add usb tethering rules"); + String[] addRules = getAdditionRules(state.usbAddress, state.usbInterface); + return runBlockingCmd(addRules, log) == 0; + } + + private boolean removeUsbTetheringRules(TetheringState state, StringBuilder log) throws Exception { + Log.d(TAG, "add usb tethering rules"); + String[] addRules = getDeletionRules(state, state.lastSeenUsbAddress, state.lastSeenUsbInterface); + return runBlockingCmd(addRules, log) == 0; + } + + //TODO: implement the follwing methods -v + private boolean removeBluetoothTetheringRules(TetheringState state, StringBuilder log) { + return true; + } + + private boolean addBluetoothTetheringRules(TetheringState state, StringBuilder log) { + return true; + } + + private String[] getAdditionRules(String addressRange, String interfaceName) { + return new String[] { + "su", + "iptables -t filter --flush " + BITMASK_FORWARD, + "iptables -t nat --flush " + BITMASK_POSTROUTING, + "iptables -t filter --append " + BITMASK_FORWARD + " --jump ACCEPT", + "iptables -t nat --append " + BITMASK_POSTROUTING + " --jump MASQUERADE", + "if [[ ! `ip rule show from "+ addressRange+" lookup 61` ]]; " + + "then ip rule add from " + addressRange + " lookup 61; " + + "fi", + "if [[ ! `ip route list table 61 | grep 'default dev " + getTunName() + " scope link'` ]]; " + + "then ip route add default dev " + getTunName() + " scope link table 61; " + + "fi", + "if [[ ! `ip route list table 61 | grep '"+ addressRange +" dev "+ interfaceName +" scope link'` ]]; " + + "then ip route add " + addressRange + " dev " + interfaceName + " scope link table 61; " + + "fi", + "if [[ ! `ip route list table 61 | grep 'broadcast 255.255.255.255 dev " + interfaceName + " scope link'` ]]; " + + "then ip route add broadcast 255.255.255.255 dev " + interfaceName + " scope link table 61; " + + "fi" + }; + } + + private String[] getDeletionRules(TetheringState state, String addressRange, String interfaceName) { + ArrayList<String> list = new ArrayList<>(); + list.add("su"); + list.add("ip route delete broadcast 255.255.255.255 dev " + addressRange +" scope link table 61"); + list.add("ip route delete " + addressRange + " dev " + interfaceName +" scope link table 61"); + if (!state.hasAnyVpnTetheringAllowed() || !state.hasAnyDeviceTetheringEnabled()) { + list.add("ip route delete default dev " + getTunName() + " scope link table 61"); + } + list.add("if [[ `ip rule show from " + addressRange + " lookup 61` ]]; " + + "then ip rule del from " + addressRange + " lookup 61; " + + "fi"); + + return list.toArray(new String[0]); + } + + + + private String getTunName() { + try { + for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { + NetworkInterface networkInterface = en.nextElement(); + if (networkInterface.getName().contains("tun")) { + return networkInterface.getName(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + FirewallCallback callback = callbackWeakReference.get(); + if (callback != null) { + callback.onTetheringStarted(result); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java new file mode 100644 index 00000000..63d6074d --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.firewall; + +import android.os.AsyncTask; +import android.util.Log; + +import java.lang.ref.WeakReference; + +import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_CHAIN; +import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; + +class ShutdownIPv6FirewallTask extends AsyncTask<Void, Boolean, Boolean> { + + private WeakReference<FirewallCallback> callbackWeakReference; + + ShutdownIPv6FirewallTask(FirewallCallback callback) { + callbackWeakReference = new WeakReference<>(callback); + } + + @Override + protected Boolean doInBackground(Void... voids) { + boolean success; + StringBuilder log = new StringBuilder(); + String[] deleteChain = new String[]{ + "su", + "id", + "ip6tables --delete OUTPUT --jump " + BITMASK_CHAIN, + "ip6tables --flush " + BITMASK_CHAIN, + "ip6tables --delete-chain " + BITMASK_CHAIN + }; + try { + success = runBlockingCmd(deleteChain, log) == 0; + } catch (Exception e) { + e.printStackTrace(); + Log.e(FirewallManager.TAG, log.toString()); + return false; + } + + try { + boolean allowSu = log.toString().contains("uid=0"); + callbackWeakReference.get().onSuRequested(allowSu); + } catch (Exception e) { + //ignore + } + return success; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + FirewallCallback callback = callbackWeakReference.get(); + if (callback != null) { + callback.onFirewallStopped(result); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java new file mode 100644 index 00000000..dcb3ccba --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.firewall; + +import android.os.AsyncTask; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.tethering.TetheringState; + +import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_FORWARD; +import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_POSTROUTING; +import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; + +public class ShutdownTetheringTask extends AsyncTask<Void, Boolean, Boolean> { + + private WeakReference<FirewallCallback> callbackWeakReference; + + ShutdownTetheringTask(FirewallCallback callback) { + callbackWeakReference = new WeakReference<>(callback); + } + + @Override + protected Boolean doInBackground(Void... args) { + TetheringState tetheringState = TetheringObservable.getInstance().getTetheringState(); + StringBuilder log = new StringBuilder(); + + String[] bitmaskChain = new String[]{ + "su", + "id", + "iptables -t filter --list " + BITMASK_FORWARD + " && iptables -t nat --list " + BITMASK_POSTROUTING }; + + try { + boolean hasBitmaskChain = runBlockingCmd(bitmaskChain, log) == 0; + boolean allowSu = log.toString().contains("uid=0"); + callbackWeakReference.get().onSuRequested(allowSu); + if (!allowSu) { + return false; + } + + log = new StringBuilder(); + + ArrayList<String> removeChains = new ArrayList<>(); + removeChains.add("su"); + removeChains.add("ip route flush table 61"); + removeChains.add("if [[ `ip rule show from " + tetheringState.lastSeenWifiAddress+ " lookup 61` ]]; " + + "then ip rule del from " + tetheringState.lastSeenWifiAddress + " lookup 61; " + + "fi"); + removeChains.add("if [[ `ip rule show from " + tetheringState.lastSeenUsbAddress+ " lookup 61` ]]; " + + "then ip rule del from " + tetheringState.lastSeenUsbAddress + " lookup 61; " + + "fi"); + if (hasBitmaskChain) { + removeChains.add("iptables -t filter --delete FORWARD --jump " + BITMASK_FORWARD); + removeChains.add("iptables -t nat --delete POSTROUTING --jump " + BITMASK_POSTROUTING); + removeChains.add("iptables -t filter --flush " + BITMASK_FORWARD); + removeChains.add("iptables -t nat --flush " + BITMASK_POSTROUTING); + removeChains.add("iptables -t filter --delete-chain " + BITMASK_FORWARD); + removeChains.add("iptables -t nat --delete-chain " + BITMASK_POSTROUTING); + } + return runBlockingCmd(removeChains.toArray(new String[0]), log) == 0; + + } catch (Exception e) { + e.printStackTrace(); + Log.e(FirewallManager.TAG, log.toString()); + } + return false; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + FirewallCallback callback = callbackWeakReference.get(); + if (callback != null) { + callback.onTetheringStarted(result); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java new file mode 100644 index 00000000..b01270e0 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.firewall; + +import android.os.AsyncTask; +import android.util.Log; + +import java.lang.ref.WeakReference; + +import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_CHAIN; +import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; + +class StartIPv6FirewallTask extends AsyncTask<Void, Boolean, Boolean> { + + private WeakReference<FirewallCallback> callbackWeakReference; + + StartIPv6FirewallTask(FirewallCallback callback) { + callbackWeakReference = new WeakReference<>(callback); + } + + @Override + protected Boolean doInBackground(Void... voids) { + StringBuilder log = new StringBuilder(); + String[] bitmaskChain = new String[]{ + "su", + "id", + "ip6tables --list " + BITMASK_CHAIN }; + + + try { + boolean hasBitmaskChain = runBlockingCmd(bitmaskChain, log) == 0; + boolean allowSu = log.toString().contains("uid=0"); + callbackWeakReference.get().onSuRequested(allowSu); + if (!allowSu) { + return false; + } + + boolean success; + log = new StringBuilder(); + if (!hasBitmaskChain) { + String[] createChainAndRules = new String[]{ + "su", + "ip6tables --new-chain " + BITMASK_CHAIN, + "ip6tables --insert OUTPUT --jump " + BITMASK_CHAIN, + "ip6tables --append " + BITMASK_CHAIN + " -p tcp --jump REJECT", + "ip6tables --append " + BITMASK_CHAIN + " -p udp --jump REJECT" + }; + success = runBlockingCmd(createChainAndRules, log) == 0; + Log.d(FirewallManager.TAG, "added " + BITMASK_CHAIN + " to ip6tables: " + success); + Log.d(FirewallManager.TAG, log.toString()); + return success; + } else { + String[] addRules = new String[] { + "su", + "ip6tables --append " + BITMASK_CHAIN + " -p tcp --jump REJECT", + "ip6tables --append " + BITMASK_CHAIN + " -p udp --jump REJECT" }; + return runBlockingCmd(addRules, log) == 0; + } + } catch (Exception e) { + e.printStackTrace(); + Log.e(FirewallManager.TAG, log.toString()); + } + return false; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + FirewallCallback callback = callbackWeakReference.get(); + if (callback != null) { + callback.onFirewallStarted(result); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java b/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java new file mode 100644 index 00000000..c53d2a6c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java @@ -0,0 +1,239 @@ +package se.leap.bitmaskclient.fragments; + +import android.app.Dialog; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatDialogFragment; +import android.support.v7.widget.AppCompatTextView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Observable; +import java.util.Observer; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.firewall.FirewallManager; +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.utils.PreferenceHelper; +import se.leap.bitmaskclient.views.IconCheckboxEntry; + +public class TetheringDialog extends AppCompatDialogFragment implements Observer { + + public final static String TAG = TetheringDialog.class.getName(); + + @InjectView(R.id.tvTitle) + AppCompatTextView title; + + @InjectView(R.id.user_message) + AppCompatTextView userMessage; + + @InjectView(R.id.selection_list_view) + RecyclerView selectionListView; + DialogListAdapter adapter; + private DialogListAdapter.ViewModel[] dataset; + + public static class DialogListAdapter extends RecyclerView.Adapter<DialogListAdapter.ViewHolder> { + + interface OnItemClickListener { + void onItemClick(ViewModel item); + } + + private ViewModel[] dataSet; + private OnItemClickListener clickListener; + + DialogListAdapter(ViewModel[] dataSet, OnItemClickListener clickListener) { + this.dataSet = dataSet; + this.clickListener = clickListener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + IconCheckboxEntry v = new IconCheckboxEntry(viewGroup.getContext()); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { + viewHolder.bind(dataSet[i], clickListener); + } + + @Override + public int getItemCount() { + return dataSet.length; + } + + public static class ViewModel { + + public Drawable image; + public String text; + public boolean checked; + public boolean enabled; + + ViewModel(Drawable image, String text, boolean checked, boolean enabled) { + this.image = image; + this.text = text; + this.checked = checked; + this.enabled = enabled; + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + ViewHolder(IconCheckboxEntry v) { + super(v); + } + + public void bind(ViewModel model, OnItemClickListener onClickListener) { + ((IconCheckboxEntry) this.itemView).bind(model); + this.itemView.setOnClickListener(v -> { + model.checked = !model.checked; + ((IconCheckboxEntry) itemView).setChecked(model.checked); + onClickListener.onItemClick(model); + }); + } + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.d_list_selection, null); + ButterKnife.inject(this, view); + + title.setText(R.string.tethering); + userMessage.setMovementMethod(LinkMovementMethod.getInstance()); + userMessage.setLinkTextColor(getContext().getResources().getColor(R.color.colorPrimary)); + userMessage.setText(createUserMessage()); + + initDataset(); + adapter = new DialogListAdapter(dataset, this::onItemClick); + selectionListView.setAdapter(adapter); + selectionListView.setLayoutManager(new LinearLayoutManager(getActivity())); + + + builder.setView(view) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + PreferenceHelper.allowWifiTethering(getContext(), dataset[0].checked); + PreferenceHelper.allowUsbTethering(getContext(), dataset[1].checked); +// PreferenceHelper.allowBluetoothTethering(getContext(), dataset[2].checked); + TetheringObservable.allowVpnWifiTethering(dataset[0].checked); + TetheringObservable.allowVpnUsbTethering(dataset[1].checked); +// TetheringObservable.allowVpnBluetoothTethering(dataset[2].checked); + FirewallManager firewallManager = new FirewallManager(getContext().getApplicationContext(), false); + if (VpnStatus.isVPNActive()) { + if (TetheringObservable.getInstance().getTetheringState().hasAnyDeviceTetheringEnabled() && + TetheringObservable.getInstance().getTetheringState().hasAnyVpnTetheringAllowed()) { + firewallManager.startTethering(); + } else { + firewallManager.stopTethering(); + } + } + }).setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); + return builder.create(); + } + + @Override + public void onResume() { + super.onResume(); + dataset[0].enabled = TetheringObservable.getInstance().isWifiTetheringEnabled(); + dataset[1].enabled = TetheringObservable.getInstance().isUsbTetheringEnabled(); +// dataset[2].enabled = TetheringObservable.getInstance().isBluetoothTetheringEnabled(); + adapter.notifyDataSetChanged(); + TetheringObservable.getInstance().addObserver(this); + } + + @Override + public void onPause() { + super.onPause(); + TetheringObservable.getInstance().deleteObserver(this); + } + + public void onItemClick(DialogListAdapter.ViewModel item) { + + } + + private CharSequence createUserMessage() { + String tetheringMessage = getString(R.string.tethering_message); + String systemSettings = getString(R.string.tethering_system_settings); + String systemSettingsMessage = getString(R.string.tethering_enabled_message, systemSettings); + String wholeMessage = systemSettingsMessage + "\n\n" + tetheringMessage; + int startIndex = wholeMessage.indexOf(systemSettings, 0); + int endIndex = startIndex + systemSettings.length(); + + Spannable spannable = new SpannableString(wholeMessage); + spannable.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + startActivity(intent); + } + }, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spannable; + } + + private void initDataset() { + dataset = new DialogListAdapter.ViewModel[] { + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_wifi), + getContext().getString(R.string.tethering_wifi), + PreferenceHelper.isWifiTetheringAllowed(getContext()), + TetheringObservable.getInstance().isWifiTetheringEnabled()), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_usb), + getContext().getString(R.string.tethering_usb), + PreferenceHelper.isUsbTetheringAllowed(getContext()), + TetheringObservable.getInstance().isUsbTetheringEnabled()), +/* new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_bluetooth), + getContext().getString(R.string.tethering_bluetooth), + PreferenceHelper.isBluetoothTetheringAllowed(getContext()), + TetheringObservable.getInstance().isUsbTetheringEnabled())*/ + }; + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TetheringObservable) { + TetheringObservable observable = (TetheringObservable) o; + Log.d(TAG, "TetheringObservable is updated"); + dataset[0].enabled = observable.isWifiTetheringEnabled(); + dataset[1].enabled = observable.isUsbTetheringEnabled(); +// dataset[2].enabled = observable.isBluetoothTetheringEnabled(); + adapter.notifyDataSetChanged(); + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java new file mode 100644 index 00000000..369a6cf6 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2020LEAP 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 <http://www.gnu.org/licenses/>. + */ + +package se.leap.bitmaskclient.tethering; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiManager; +import android.util.Log; + +public class TetheringBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = TetheringBroadcastReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if ("android.net.wifi.WIFI_AP_STATE_CHANGED".equals(intent.getAction())) { + Log.d(TAG, "TETHERING WIFI_AP_STATE_CHANGED"); + TetheringStateManager.updateWifiTetheringState(); + } else if ("android.net.conn.TETHER_STATE_CHANGED".equals(intent.getAction())) { + Log.d(TAG, "TETHERING TETHER_STATE_CHANGED"); + TetheringStateManager.updateUsbTetheringState(); + TetheringStateManager.updateBluetoothTetheringState(); + TetheringStateManager.updateWifiTetheringState(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringObservable.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringObservable.java new file mode 100644 index 00000000..75d29417 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringObservable.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +import android.support.annotation.NonNull; + +import java.util.Observable; + +public class TetheringObservable extends Observable { + private static TetheringObservable instance; + + private TetheringState tetheringState; + + private TetheringObservable() { + tetheringState = new TetheringState(); + } + + public static TetheringObservable getInstance() { + if (instance == null) { + instance = new TetheringObservable(); + } + return instance; + } + + public static void allowVpnWifiTethering(boolean enabled) { + if (getInstance().tetheringState.isVpnWifiTetheringAllowed != enabled) { + getInstance().tetheringState.isVpnWifiTetheringAllowed = enabled; + getInstance().setChanged(); + getInstance().notifyObservers(); + } + } + + public static void allowVpnUsbTethering(boolean enabled) { + if (getInstance().tetheringState.isVpnUsbTetheringAllowed != enabled) { + getInstance().tetheringState.isVpnUsbTetheringAllowed = enabled; + getInstance().setChanged(); + getInstance().notifyObservers(); + } + } + + public static void allowVpnBluetoothTethering(boolean enabled) { + if (getInstance().tetheringState.isVpnBluetoothTetheringAllowed != enabled) { + getInstance().tetheringState.isVpnBluetoothTetheringAllowed = enabled; + getInstance().setChanged(); + getInstance().notifyObservers(); + } + } + + static void setWifiTethering(boolean enabled, @NonNull String address, @NonNull String interfaceName) { + if (getInstance().tetheringState.isWifiTetheringEnabled != enabled || + !getInstance().tetheringState.wifiInterface.equals(interfaceName) || + !getInstance().tetheringState.wifiAddress.equals(address)) { + TetheringState state = getInstance().tetheringState; + state.isWifiTetheringEnabled = enabled; + state.wifiInterface = interfaceName; + state.wifiAddress = address; + state.lastSeenWifiAddress = address.isEmpty() ? state.lastSeenWifiAddress : address; + state.lastSeenWifiInterface = interfaceName.isEmpty() ? state.lastSeenWifiInterface : interfaceName; + getInstance().setChanged(); + getInstance().notifyObservers(); + } + + } + + static void setUsbTethering(boolean enabled, @NonNull String address, @NonNull String interfaceName) { + if (getInstance().tetheringState.isUsbTetheringEnabled != enabled || + !getInstance().tetheringState.usbAddress.equals(address) || + !getInstance().tetheringState.usbInterface.equals(interfaceName)) { + TetheringState state = getInstance().tetheringState; + state.isUsbTetheringEnabled = enabled; + state.usbAddress = address; + state.usbInterface = interfaceName; + state.lastSeenUsbAddress = address.isEmpty() ? state.lastSeenUsbAddress : address; + state.lastSeenUsbInterface = interfaceName.isEmpty() ? state.lastSeenUsbInterface : interfaceName; + getInstance().setChanged(); + getInstance().notifyObservers(); + } + } + + static void setBluetoothTethering(boolean enabled) { + if (getInstance().tetheringState.isBluetoothTetheringEnabled != enabled) { + getInstance().tetheringState.isBluetoothTetheringEnabled = enabled; + getInstance().setChanged(); + getInstance().notifyObservers(); + } + } + + public boolean isBluetoothTetheringEnabled() { + return tetheringState.isBluetoothTetheringEnabled; + } + + public boolean isUsbTetheringEnabled() { + return tetheringState.isUsbTetheringEnabled; + } + + public boolean isWifiTetheringEnabled() { + return tetheringState.isWifiTetheringEnabled; + } + + public TetheringState getTetheringState() { + return tetheringState; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringState.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringState.java new file mode 100644 index 00000000..8ef237c6 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringState.java @@ -0,0 +1,45 @@ +package se.leap.bitmaskclient.tethering; + +public class TetheringState implements Cloneable { + public boolean isWifiTetheringEnabled; + public boolean isUsbTetheringEnabled; + public boolean isBluetoothTetheringEnabled; + public boolean isVpnWifiTetheringAllowed; + public boolean isVpnUsbTetheringAllowed; + public boolean isVpnBluetoothTetheringAllowed; + public String wifiInterface = ""; + public String lastSeenWifiInterface = ""; + public String wifiAddress = ""; + public String lastSeenWifiAddress = ""; + public String usbInterface = ""; + public String lastSeenUsbInterface = ""; + public String usbAddress = ""; + public String lastSeenUsbAddress = ""; + public String bluetoothInterface = ""; + public String lastSeenBluetoothInterface = ""; + public String bluetoothAddress = ""; + public String lastSeenBluetoothAddress = ""; + + + public boolean tetherWifiVpn() { + return isWifiTetheringEnabled && isVpnWifiTetheringAllowed; + } + + public boolean tetherUsbVpn() { + return isUsbTetheringEnabled && isVpnUsbTetheringAllowed; + } + + public boolean tetherBluetoothVpn() { + return isBluetoothTetheringEnabled && isVpnBluetoothTetheringAllowed; + } + + public boolean hasAnyDeviceTetheringEnabled() { + return isBluetoothTetheringEnabled || isUsbTetheringEnabled || isWifiTetheringEnabled; + } + + public boolean hasAnyVpnTetheringAllowed() { + return isVpnWifiTetheringAllowed || isVpnUsbTetheringAllowed || isVpnBluetoothTetheringAllowed; + } + + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java new file mode 100644 index 00000000..d3c934f6 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +import android.content.Context; +import android.content.IntentFilter; +import android.support.annotation.VisibleForTesting; + +import java.net.Inet4Address; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; +import java.util.List; + +import se.leap.bitmaskclient.utils.Cmd; + +import static se.leap.bitmaskclient.utils.PreferenceHelper.isBluetoothTetheringAllowed; +import static se.leap.bitmaskclient.utils.PreferenceHelper.isUsbTetheringAllowed; +import static se.leap.bitmaskclient.utils.PreferenceHelper.isWifiTetheringAllowed; + +/** + * This manager tries to figure out the current tethering states for Wifi, USB and Bluetooth + * The default behavior differs for failing attempts to get these states: + * Wifi: keeps old state + * USB: defaults to false + * Bluetooth defaults to false + * For Wifi there's a second method to check the current state (see TetheringBroadcastReceiver). + * Either of both methods can change the state if they succeed, but are ignored if they fail. + * This should avoid any interference between both methods. + */ +public class TetheringStateManager { + private static final String TAG = TetheringStateManager.class.getSimpleName(); + private static TetheringStateManager instance; + + private WifiManagerWrapper wifiManager; + + private TetheringStateManager() { } + + public static TetheringStateManager getInstance() { + if (instance == null) { + instance = new TetheringStateManager(); + } + return instance; + } + + public void init(Context context) { + TetheringBroadcastReceiver broadcastReceiver = new TetheringBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter("android.net.conn.TETHER_STATE_CHANGED"); + intentFilter.addAction("android.net.wifi.WIFI_AP_STATE_CHANGED"); + context.getApplicationContext().registerReceiver(broadcastReceiver, intentFilter); + instance.wifiManager = new WifiManagerWrapper(context); + TetheringObservable.allowVpnWifiTethering(isWifiTetheringAllowed(context)); + TetheringObservable.allowVpnUsbTethering(isUsbTetheringAllowed(context)); + TetheringObservable.allowVpnBluetoothTethering(isBluetoothTetheringAllowed(context)); + updateWifiTetheringState(); + updateUsbTetheringState(); + updateBluetoothTetheringState(); + } + + static void updateWifiTetheringState() { + WifiManagerWrapper manager = getInstance().wifiManager; + try { + TetheringObservable.setWifiTethering(manager.isWifiAPEnabled(), getWifiAddressRange(), getWlanInterfaceName()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + static void updateUsbTetheringState() { + TetheringObservable.setUsbTethering(isUsbTetheringEnabled(), getUsbAddressRange(), getUsbInterfaceName()); + } + + static void updateBluetoothTetheringState() { + //TetheringObservable.setBluetoothTethering(isBluetoothTetheringEnabled()); + } + + private static String getWifiAddressRange() { + String interfaceAddress = getInterfaceAddress(getWlanInterface()); + return getAddressRange(interfaceAddress); + } + + private static String getUsbAddressRange() { + String interfaceAddress = getInterfaceAddress(getUsbInterface()); + return getAddressRange(interfaceAddress); + } + + private static String getWlanInterfaceName() { + return getInterfaceName(getWlanInterface()); + } + + private static String getUsbInterfaceName() { + return getInterfaceName(getUsbInterface()); + } + + private static NetworkInterface getWlanInterface() { + return getNetworkInterface(new String[]{"wlan", "eth"}); + } + + private static NetworkInterface getUsbInterface() { + return getNetworkInterface(new String[]{"rndis", "usb"}); + } + + private static boolean isBluetoothTetheringEnabled() { + StringBuilder log = new StringBuilder(); + boolean hasBtPan = false; + try { + hasBtPan = Cmd.runBlockingCmd(new String[] {"ifconfig bt-pan"}, log) == 0; + //Log.d(TAG, "ifconfig result: " + log.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + return hasBtPan; + + } + + private static boolean isUsbTetheringEnabled() { + return getUsbInterface() != null; + } + + private static String getAddressRange(String interfaceAddress) { + if (interfaceAddress.split("\\.").length == 4) { + String result = interfaceAddress.substring(0, interfaceAddress.lastIndexOf(".")); + result = result + ".0/24"; + return result; + } + return ""; + } + + private static String getInterfaceAddress(NetworkInterface networkInterface) { + if (networkInterface != null) { + List<InterfaceAddress> ifaceAddresses = networkInterface.getInterfaceAddresses(); + for (InterfaceAddress ifaceAddres : ifaceAddresses) { + if (ifaceAddres.getAddress() instanceof Inet4Address) { + return ifaceAddres.getAddress().getHostAddress(); + } + } + } + return ""; + } + + private static String getInterfaceName(NetworkInterface networkInterface) { + if (networkInterface != null) { + return networkInterface.getName(); + } + return ""; + } + + private static NetworkInterface getNetworkInterface(String[] interfaceNames) { + try { + for(Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) { + NetworkInterface networkInterface = en.nextElement(); + if(!networkInterface.isLoopback()){ + for (String interfaceName : interfaceNames) { + if (networkInterface.getName().contains(interfaceName)) { + return networkInterface; + } + } + } + } + } catch(Exception e){ + e.printStackTrace(); + } + + return null; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/WifiHotspotState.java b/app/src/main/java/se/leap/bitmaskclient/tethering/WifiHotspotState.java new file mode 100644 index 00000000..f29a87f8 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/WifiHotspotState.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +public enum WifiHotspotState { + WIFI_AP_STATE_DISABLING, + WIFI_AP_STATE_DISABLED, + WIFI_AP_STATE_ENABLING, + WIFI_AP_STATE_ENABLED, + WIFI_AP_STATE_FAILED +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java b/app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java new file mode 100644 index 00000000..ed395d7f --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +import android.content.Context; +import android.net.wifi.WifiManager; + +import java.lang.reflect.Method; + +/** + * This Wrapper allows better Unit testing. + */ +class WifiManagerWrapper { + + private WifiManager wifiManager; + + WifiManagerWrapper(Context context) { + this.wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + + boolean isWifiAPEnabled() throws Exception { + Method method = wifiManager.getClass().getMethod("getWifiApState"); + int tmp = ((Integer) method.invoke(wifiManager)); + return WifiHotspotState.WIFI_AP_STATE_ENABLED.ordinal() == tmp % 10; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java b/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java index a72658a4..d033ed24 100644 --- a/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java +++ b/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java @@ -18,7 +18,6 @@ package se.leap.bitmaskclient.utils; import android.support.annotation.WorkerThread; -import android.util.Log; import java.io.IOException; import java.io.InputStreamReader; @@ -43,7 +42,6 @@ public class Cmd { try { for (String cmd : cmds) { - Log.d(TAG, "executing CMD: " + cmd); out.write(cmd); out.write("\n"); } diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/FirewallHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/FirewallHelper.java deleted file mode 100644 index 26e6603a..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/FirewallHelper.java +++ /dev/null @@ -1,196 +0,0 @@ -package se.leap.bitmaskclient.utils; -/** - * Copyright (c) 2019 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 <http://www.gnu.org/licenses/>. - */ - -import android.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import java.lang.ref.WeakReference; - -import de.blinkt.openvpn.core.VpnStatus; - -import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; - -interface FirewallCallback { - void onFirewallStarted(boolean success); - void onFirewallStopped(boolean success); - void onSuRequested(boolean success); -} - - -public class FirewallHelper implements FirewallCallback { - private static String BITMASK_CHAIN = "bitmask_fw"; - private static final String TAG = FirewallHelper.class.getSimpleName(); - - private Context context; - - public FirewallHelper(Context context) { - this.context = context; - } - - - @Override - public void onFirewallStarted(boolean success) { - if (success) { - VpnStatus.logInfo("[FIREWALL] Custom rules established"); - } else { - VpnStatus.logError("[FIREWALL] Could not establish custom rules."); - } - } - - @Override - public void onFirewallStopped(boolean success) { - if (success) { - VpnStatus.logInfo("[FIREWALL] Custom rules deleted"); - } else { - VpnStatus.logError("[FIREWALL] Could not delete custom rules"); - } - } - - @Override - public void onSuRequested(boolean success) { - PreferenceHelper.setSuPermission(context, success); - if (!success) { - VpnStatus.logError("[FIREWALL] Root permission needed to execute custom firewall rules."); - } - } - - - private static class StartFirewallTask extends AsyncTask<Void, Boolean, Boolean> { - - WeakReference<FirewallCallback> callbackWeakReference; - - StartFirewallTask(FirewallCallback callback) { - callbackWeakReference = new WeakReference<>(callback); - } - - @Override - protected Boolean doInBackground(Void... voids) { - StringBuilder log = new StringBuilder(); - String[] bitmaskChain = new String[]{ - "su", - "id", - "ip6tables --list " + BITMASK_CHAIN }; - - - try { - boolean hasBitmaskChain = runBlockingCmd(bitmaskChain, log) == 0; - boolean allowSu = log.toString().contains("uid=0"); - try { - callbackWeakReference.get().onSuRequested(allowSu); - Thread.sleep(1000); - } catch (Exception e) { - //ignore - } - - boolean success; - log = new StringBuilder(); - if (!hasBitmaskChain) { - String[] createChainAndRules = new String[]{ - "su", - "ip6tables --new-chain " + BITMASK_CHAIN, - "ip6tables --insert OUTPUT --jump " + BITMASK_CHAIN, - "ip6tables --append " + BITMASK_CHAIN + " -p tcp --jump REJECT", - "ip6tables --append " + BITMASK_CHAIN + " -p udp --jump REJECT" - }; - success = runBlockingCmd(createChainAndRules, log) == 0; - Log.d(TAG, "added " + BITMASK_CHAIN + " to ip6tables: " + success); - Log.d(TAG, log.toString()); - return success; - } else { - String[] addRules = new String[] { - "su", - "ip6tables --append " + BITMASK_CHAIN + " -p tcp --jump REJECT", - "ip6tables --append " + BITMASK_CHAIN + " -p udp --jump REJECT" }; - return runBlockingCmd(addRules, log) == 0; - } - } catch (Exception e) { - e.printStackTrace(); - Log.e(TAG, log.toString()); - } - return false; - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - FirewallCallback callback = callbackWeakReference.get(); - if (callback != null) { - callback.onFirewallStarted(result); - } - } - } - - private static class ShutdownFirewallTask extends AsyncTask<Void, Boolean, Boolean> { - - WeakReference<FirewallCallback> callbackWeakReference; - - ShutdownFirewallTask(FirewallCallback callback) { - callbackWeakReference = new WeakReference<>(callback); - } - - @Override - protected Boolean doInBackground(Void... voids) { - boolean success; - StringBuilder log = new StringBuilder(); - String[] deleteChain = new String[]{ - "su", - "id", - "ip6tables --delete OUTPUT --jump " + BITMASK_CHAIN, - "ip6tables --flush " + BITMASK_CHAIN, - "ip6tables --delete-chain " + BITMASK_CHAIN - }; - try { - success = runBlockingCmd(deleteChain, log) == 0; - } catch (Exception e) { - e.printStackTrace(); - Log.e(TAG, log.toString()); - return false; - } - - try { - boolean allowSu = log.toString().contains("uid=0"); - callbackWeakReference.get().onSuRequested(allowSu); - } catch (Exception e) { - //ignore - } - return success; - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - FirewallCallback callback = callbackWeakReference.get(); - if (callback != null) { - callback.onFirewallStopped(result); - } - } - } - - - public void startFirewall() { - StartFirewallTask task = new StartFirewallTask(this); - task.execute(); - } - - public void shutdownFirewall() { - ShutdownFirewallTask task = new ShutdownFirewallTask(this); - task.execute(); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java index de2058c7..6f9744bc 100644 --- a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java @@ -3,35 +3,34 @@ package se.leap.bitmaskclient.utils; import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Set; import de.blinkt.openvpn.VpnProfile; import se.leap.bitmaskclient.Provider; import static android.content.Context.MODE_PRIVATE; +import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_BLUETOOTH; +import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_USB; +import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_WIFI; import static se.leap.bitmaskclient.Constants.ALWAYS_ON_SHOW_DIALOG; import static se.leap.bitmaskclient.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER; import static se.leap.bitmaskclient.Constants.EXCLUDED_APPS; import static se.leap.bitmaskclient.Constants.LAST_USED_PROFILE; -import static se.leap.bitmaskclient.Constants.PREFERENCES_APP_VERSION; import static se.leap.bitmaskclient.Constants.PROVIDER_CONFIGURED; import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION; import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.Constants.SHOW_EXPERIMENTAL; import static se.leap.bitmaskclient.Constants.SU_PERMISSION; +import static se.leap.bitmaskclient.Constants.USE_IPv6_FIREWALL; import static se.leap.bitmaskclient.Constants.USE_PLUGGABLE_TRANSPORTS; /** @@ -146,6 +145,46 @@ public class PreferenceHelper { return getBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, false); } + public static void allowUsbTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_USB, isEnabled); + } + + public static boolean isUsbTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_USB, false); + } + + public static void allowWifiTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_WIFI, isEnabled); + } + + public static boolean isWifiTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_WIFI, false); + } + + public static void allowBluetoothTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_BLUETOOTH, isEnabled); + } + + public static boolean isBluetoothTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_BLUETOOTH, false); + } + + public static void setShowExperimentalFeatures(Context context, boolean show) { + putBoolean(context, SHOW_EXPERIMENTAL, show); + } + + public static boolean showExperimentalFeatures(Context context) { + return getBoolean(context, SHOW_EXPERIMENTAL, false); + } + + public static void setUseIPv6Firewall(Context context, boolean useFirewall) { + putBoolean(context, USE_IPv6_FIREWALL, useFirewall); + } + + public static boolean useIpv6Firewall(Context context) { + return getBoolean(context, USE_IPv6_FIREWALL, false); + } + public static void saveShowAlwaysOnDialog(Context context, boolean showAlwaysOnDialog) { putBoolean(context, ALWAYS_ON_SHOW_DIALOG, showAlwaysOnDialog); } diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java new file mode 100644 index 00000000..ca44592e --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java @@ -0,0 +1,86 @@ +package se.leap.bitmaskclient.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.fragments.TetheringDialog; + + +public class IconCheckboxEntry extends LinearLayout { + + @InjectView(android.R.id.text1) + TextView textView; + + @InjectView(R.id.material_icon) + AppCompatImageView iconView; + + @InjectView(R.id.checked_icon) + AppCompatImageView checkedIcon; + + public IconCheckboxEntry(Context context) { + super(context); + initLayout(context, null); + } + + public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initLayout(context, attrs); + } + + public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLayout(context, attrs); + } + + @TargetApi(21) + public IconCheckboxEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initLayout(context, attrs); + } + + void initLayout(Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rootview = inflater.inflate(R.layout.v_icon_select_text_list_item, this, true); + ButterKnife.inject(this, rootview); + + + + } + + public void bind(TetheringDialog.DialogListAdapter.ViewModel model) { + this.setEnabled(model.enabled); + textView.setText(model.text); + textView.setEnabled(model.enabled); + + Drawable checkIcon = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_check_bold)).mutate(); + if (model.enabled) { + DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorSuccess)); + } else { + DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorDisabled)); + } + + iconView.setImageDrawable(model.image); + checkedIcon.setImageDrawable(checkIcon); + setChecked(model.checked); + } + + public void setChecked(boolean checked) { + checkedIcon.setVisibility(checked ? VISIBLE : GONE); + checkedIcon.setContentDescription(checked ? "selected" : "unselected"); + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_access_point_36.png b/app/src/main/res/drawable-hdpi/ic_access_point_36.png Binary files differnew file mode 100644 index 00000000..03444d0f --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_access_point_36.png diff --git a/app/src/main/res/drawable-hdpi/ic_bluetooth.png b/app/src/main/res/drawable-hdpi/ic_bluetooth.png Binary files differnew file mode 100644 index 00000000..e7c1589b --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_bluetooth.png diff --git a/app/src/main/res/drawable-hdpi/ic_cancel.png b/app/src/main/res/drawable-hdpi/ic_cancel.png Binary files differnew file mode 100644 index 00000000..579b1dff --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_cancel.png diff --git a/app/src/main/res/drawable-hdpi/ic_check_bold.png b/app/src/main/res/drawable-hdpi/ic_check_bold.png Binary files differnew file mode 100644 index 00000000..28418346 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_check_bold.png diff --git a/app/src/main/res/drawable-hdpi/ic_usb.png b/app/src/main/res/drawable-hdpi/ic_usb.png Binary files differnew file mode 100644 index 00000000..b9de586f --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_usb.png diff --git a/app/src/main/res/drawable-hdpi/ic_wifi.png b/app/src/main/res/drawable-hdpi/ic_wifi.png Binary files differnew file mode 100644 index 00000000..ca6b94a3 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_wifi.png diff --git a/app/src/main/res/drawable-ldpi/ic_bluetooth.png b/app/src/main/res/drawable-ldpi/ic_bluetooth.png Binary files differnew file mode 100644 index 00000000..3a73c82f --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ic_bluetooth.png diff --git a/app/src/main/res/drawable-ldpi/ic_cancel.png b/app/src/main/res/drawable-ldpi/ic_cancel.png Binary files differnew file mode 100644 index 00000000..a396f18e --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ic_cancel.png diff --git a/app/src/main/res/drawable-ldpi/ic_check_bold.png b/app/src/main/res/drawable-ldpi/ic_check_bold.png Binary files differnew file mode 100644 index 00000000..4f765ed4 --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ic_check_bold.png diff --git a/app/src/main/res/drawable-ldpi/ic_usb.png b/app/src/main/res/drawable-ldpi/ic_usb.png Binary files differnew file mode 100644 index 00000000..d48d2f50 --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ic_usb.png diff --git a/app/src/main/res/drawable-ldpi/ic_wifi.png b/app/src/main/res/drawable-ldpi/ic_wifi.png Binary files differnew file mode 100644 index 00000000..56ad6403 --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ic_wifi.png diff --git a/app/src/main/res/drawable-mdpi/ic_access_point_36.png b/app/src/main/res/drawable-mdpi/ic_access_point_36.png Binary files differnew file mode 100644 index 00000000..c461a0a5 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_access_point_36.png diff --git a/app/src/main/res/drawable-mdpi/ic_bluetooth.png b/app/src/main/res/drawable-mdpi/ic_bluetooth.png Binary files differnew file mode 100644 index 00000000..a1cecd2b --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_bluetooth.png diff --git a/app/src/main/res/drawable-mdpi/ic_cancel.png b/app/src/main/res/drawable-mdpi/ic_cancel.png Binary files differnew file mode 100644 index 00000000..125c82f1 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_cancel.png diff --git a/app/src/main/res/drawable-mdpi/ic_check_bold.png b/app/src/main/res/drawable-mdpi/ic_check_bold.png Binary files differnew file mode 100644 index 00000000..872ef957 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_check_bold.png diff --git a/app/src/main/res/drawable-mdpi/ic_usb.png b/app/src/main/res/drawable-mdpi/ic_usb.png Binary files differnew file mode 100644 index 00000000..3ad5ebc1 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_usb.png diff --git a/app/src/main/res/drawable-mdpi/ic_wifi.png b/app/src/main/res/drawable-mdpi/ic_wifi.png Binary files differnew file mode 100644 index 00000000..21a69023 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_wifi.png diff --git a/app/src/main/res/drawable-xhdpi/ic_access_point_36.png b/app/src/main/res/drawable-xhdpi/ic_access_point_36.png Binary files differnew file mode 100644 index 00000000..4ae3d1d9 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_access_point_36.png diff --git a/app/src/main/res/drawable-xhdpi/ic_bluetooth.png b/app/src/main/res/drawable-xhdpi/ic_bluetooth.png Binary files differnew file mode 100644 index 00000000..32a854e5 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_bluetooth.png diff --git a/app/src/main/res/drawable-xhdpi/ic_cancel.png b/app/src/main/res/drawable-xhdpi/ic_cancel.png Binary files differnew file mode 100644 index 00000000..aac09d65 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_cancel.png diff --git a/app/src/main/res/drawable-xhdpi/ic_check_bold.png b/app/src/main/res/drawable-xhdpi/ic_check_bold.png Binary files differnew file mode 100644 index 00000000..da6a1ecb --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_check_bold.png diff --git a/app/src/main/res/drawable-xhdpi/ic_usb.png b/app/src/main/res/drawable-xhdpi/ic_usb.png Binary files differnew file mode 100644 index 00000000..c11940b1 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_usb.png diff --git a/app/src/main/res/drawable-xhdpi/ic_wifi.png b/app/src/main/res/drawable-xhdpi/ic_wifi.png Binary files differnew file mode 100644 index 00000000..cf1bb909 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_wifi.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png b/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png Binary files differnew file mode 100644 index 00000000..9c30e5b4 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxhdpi/ic_cancel.png Binary files differnew file mode 100644 index 00000000..1e9bce6c --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_cancel.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_check_bold.png b/app/src/main/res/drawable-xxhdpi/ic_check_bold.png Binary files differnew file mode 100644 index 00000000..f6b50706 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_check_bold.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_usb.png b/app/src/main/res/drawable-xxhdpi/ic_usb.png Binary files differnew file mode 100644 index 00000000..c78e33af --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_usb.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_wifi.png b/app/src/main/res/drawable-xxhdpi/ic_wifi.png Binary files differnew file mode 100644 index 00000000..ea9e08a9 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_wifi.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_access_point_36.png b/app/src/main/res/drawable-xxxhdpi/ic_access_point_36.png Binary files differnew file mode 100644 index 00000000..4a2f25c1 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_access_point_36.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png b/app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png Binary files differnew file mode 100644 index 00000000..6eccbbd6 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxxhdpi/ic_cancel.png Binary files differnew file mode 100644 index 00000000..4ef00efc --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_cancel.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_check_bold.png b/app/src/main/res/drawable-xxxhdpi/ic_check_bold.png Binary files differnew file mode 100644 index 00000000..19029a0d --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_check_bold.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_usb.png b/app/src/main/res/drawable-xxxhdpi/ic_usb.png Binary files differnew file mode 100644 index 00000000..4bebd840 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_usb.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_wifi.png b/app/src/main/res/drawable-xxxhdpi/ic_wifi.png Binary files differnew file mode 100644 index 00000000..b5a05f7a --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_wifi.png diff --git a/app/src/main/res/layout/d_list_selection.xml b/app/src/main/res/layout/d_list_selection.xml new file mode 100644 index 00000000..ef963303 --- /dev/null +++ b/app/src/main/res/layout/d_list_selection.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical"> + + <android.support.v7.widget.AppCompatTextView + android:id="@+id/tvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/standard_margin" + android:layout_marginTop="@dimen/add_button_margin" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:textAllCaps="true" + android:textAppearance="@style/TextAppearance.AppCompat.Title" + android:textStyle="bold" + /> + + <android.support.v7.widget.AppCompatTextView + android:id="@+id/user_message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginBottom="0dp" + tools:text="@string/tethering_message" + android:textSize="17sp" + /> + + <android.support.v7.widget.RecyclerView + android:id="@+id/selection_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawSelectorOnTop="false" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/standard_margin" + android:layout_marginBottom="@dimen/standard_margin" + android:visibility="visible" + tools:visibility="visible" + /> + + </LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml index f6c9b2bb..191d547f 100644 --- a/app/src/main/res/layout/f_drawer_main.xml +++ b/app/src/main/res/layout/f_drawer_main.xml @@ -97,10 +97,47 @@ android:visibility="gone" /> + <TextView + android:id="@+id/show_experimental_features" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/show_experimental" + android:textColor="@color/colorPrimaryDark" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:gravity="center" + android:background="@color/black800_high_transparent" + /> + + <se.leap.bitmaskclient.views.IconSwitchEntry + android:id="@+id/enableIPv6Firewall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:text="@string/ipv6Firewall" + app:subtitle="@string/require_root" + app:icon="@drawable/ic_cancel" + android:visibility="gone" + tools:visibility="visible" + /> + + <se.leap.bitmaskclient.views.IconTextEntry + android:id="@+id/tethering" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:text="@string/tethering" + app:subtitle="@string/require_root" + app:icon="@drawable/ic_access_point_36" + android:visibility="gone" + tools:visibility="visible" + /> + <View + android:id="@+id/experimental_features_footer" android:layout_width="match_parent" android:layout_height="20dp" android:background="@color/black800_high_transparent" + android:visibility="gone" + tools:visibility="visible" /> <se.leap.bitmaskclient.views.IconTextEntry diff --git a/app/src/main/res/layout/v_icon_select_text_list_item.xml b/app/src/main/res/layout/v_icon_select_text_list_item.xml new file mode 100644 index 00000000..2fa56b46 --- /dev/null +++ b/app/src/main/res/layout/v_icon_select_text_list_item.xml @@ -0,0 +1,53 @@ +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/item_container" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:layout_width="match_parent" + android:orientation="horizontal" + xmlns:tools="http://schemas.android.com/tools"> + + <android.support.v7.widget.AppCompatImageView + android:id="@+id/material_icon" + android:layout_width="?android:attr/listPreferredItemHeightSmall" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:padding="6dp" + android:layout_gravity="center" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_bluetooth" + /> + + <TextView + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:gravity="center_vertical" + android:paddingStart="4dp" + android:paddingLeft="4dp" + android:paddingEnd="4dp" + android:paddingRight="4dp" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + tools:text="TEST" + android:layout_toEndOf="@id/material_icon" + android:layout_toRightOf="@+id/material_icon" + /> + + <android.support.v7.widget.AppCompatImageView + android:id="@+id/checked_icon" + android:layout_width="?android:attr/listPreferredItemHeightSmall" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:layout_gravity="center" + android:padding="10dp" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + tools:src="@drawable/ic_check_bold" + android:visibility="visible" + tools:visibility="visible" + /> + + <View + android:layout_width="match_parent" + android:layout_height="1px" + android:background="@android:color/darker_gray" + android:layout_alignParentBottom="true" + /> +</RelativeLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b52e34dd..b5fc2fa2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,6 +105,17 @@ <string name="save_battery_message">Background data connections will hibernate when your phone is inactive.</string> <string name="always_on_vpn">Always-on VPN</string> <string name="subtitle_always_on_vpn">Open Android System Settings</string> + <string name="tethering">VPN Hotspot</string> + <string name="ipv6Firewall">Block IPv6</string> + <string name="require_root">Requires root permissions</string> + <string name="show_experimental">Show experimental options</string> + <string name="hide_experimental">Hide experimental options</string> + <string name="tethering_enabled_message">Please make sure to enable tethering in the %s first!</string> + <string name="tethering_system_settings">system settings</string> + <string name="tethering_message">Share your VPN with other devices via:</string> + <string name="tethering_wifi">Wifi hotspot</string> + <string name="tethering_usb">USB tethering</string> + <string name="tethering_bluetooth">Bluetooth tethering</string> <string name="do_not_show_again">Do not show again</string> <string name="always_on_vpn_user_message">To enable always-on VPN in Android VPN Settings click on the configure icon [img src] and turn the switch on.</string> <string name="always_on_blocking_vpn_user_message">To protect your privacy optimally, you should also activate the option \"Block connections without VPN\".</string> diff --git a/app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java new file mode 100644 index 00000000..b3ab75ba --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java @@ -0,0 +1,323 @@ +/** + * Copyright (c) 2020 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 <http://www.gnu.org/licenses/>. + */ +package se.leap.bitmaskclient.tethering; + +import android.content.Context; +import android.content.IntentFilter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; + +import se.leap.bitmaskclient.utils.Cmd; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + + +@RunWith(PowerMockRunner.class) +@PrepareForTest({WifiManagerWrapper.class, TetheringStateManager.class, Cmd.class, NetworkInterface.class}) +public class TetheringStateManagerTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + Context mockContext; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + IntentFilter intentFilter; + + private TetheringObservable observable; + + @Before + public void setup() throws Exception { + PowerMockito.whenNew(IntentFilter.class).withArguments(anyString()).thenReturn(intentFilter); + PowerMockito.whenNew(IntentFilter.class).withNoArguments().thenReturn(intentFilter); + observable = TetheringObservable.getInstance(); + + } + + @Test + public void updateUsbTetheringState_findsRndisX_returnsTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + PowerMockito.mockStatic(NetworkInterface.class); + NetworkInterface mock1 = PowerMockito.mock(NetworkInterface.class); + when(mock1.isLoopback()).thenReturn(false); + when(mock1.getName()).thenReturn("eth0"); + NetworkInterface mock2 = PowerMockito.mock(NetworkInterface.class); + when(mock2.isLoopback()).thenReturn(false); + when(mock2.getName()).thenReturn("rndis0"); + + NetworkInterface[] networkInterfaces = new NetworkInterface[2]; + networkInterfaces[0] = mock1; + networkInterfaces[1] = mock2; + + PowerMockito.when(NetworkInterface.getNetworkInterfaces()).then(new Answer<Enumeration<NetworkInterface>>() { + @Override + public Enumeration<NetworkInterface> answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(Arrays.asList(networkInterfaces)); + } + }); + + TetheringObservable.setUsbTethering(false, "192.168.42.0/24", "rndis0"); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isUsbTetheringEnabled()); + } + + @Test + public void updateUsbTetheringState_doesntFindRndisX_returnsFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + PowerMockito.mockStatic(NetworkInterface.class); + NetworkInterface mock1 = PowerMockito.mock(NetworkInterface.class); + when(mock1.isLoopback()).thenReturn(false); + when(mock1.getName()).thenReturn("eth0"); + NetworkInterface mock2 = PowerMockito.mock(NetworkInterface.class); + when(mock2.isLoopback()).thenReturn(false); + when(mock2.getName()).thenReturn("wifi0"); + + NetworkInterface[] networkInterfaces = new NetworkInterface[2]; + networkInterfaces[0] = mock1; + networkInterfaces[1] = mock2; + + PowerMockito.when(NetworkInterface.getNetworkInterfaces()).then(new Answer<Enumeration<NetworkInterface>>() { + @Override + public Enumeration<NetworkInterface> answer(InvocationOnMock invocation) throws Throwable { + return Collections.enumeration(Arrays.asList(networkInterfaces)); + } + }); + + TetheringObservable.setUsbTethering(true, "192.168.42.0/24", "rndis0"); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isUsbTetheringEnabled()); + } + + @Test + public void updateUsbTetheringState_ThrowsException_returnsFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + PowerMockito.mockStatic(NetworkInterface.class); + PowerMockito.when(NetworkInterface.getNetworkInterfaces()).thenThrow(new SocketException()); + + TetheringObservable.setUsbTethering(true, "192.168.42.0/24", "rndis0"); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isUsbTetheringEnabled()); + } + +/* //TODO enable these tests as soon as bluetooth tethering has been enabled again + @Test + public void updateBluetoothTetheringState_btDeviceFound_returnTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))).then(new Answer<Integer>() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StringBuilder logStringBuilder = invocation.getArgument(1); + logStringBuilder.append("bt-pan device found"); + return 0; + } + }); + + TetheringObservable.setBluetoothTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isBluetoothTetheringEnabled()); + } + + + @Test + public void updateBluetoothTetheringState_btPanDeviceNotFound_returnFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))).then(new Answer<Integer>() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StringBuilder logStringBuilder = invocation.getArgument(1); + logStringBuilder.append("bt-pan device not found"); + return 1; + } + }); + + TetheringObservable.setBluetoothTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isBluetoothTetheringEnabled()); + } + + @Test + public void updateBluetoothTetheringState_ThrowsException_returnsFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))). + thenThrow(new SecurityException("Creation of subprocess is not allowed")); + + TetheringObservable.setBluetoothTethering(true); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isBluetoothTetheringEnabled()); + } + + @Test + public void updateBluetoothTetheringState_WifiManagerWrapperThrowsException_hasNoInfluenceOnResult() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenThrow(new NoSuchMethodException()); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + mockStatic(Cmd.class); + PowerMockito.when(Cmd.runBlockingCmd(any(), any(StringBuilder.class))).then(new Answer<Integer>() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + StringBuilder logStringBuilder = invocation.getArgument(1); + logStringBuilder.append("bt-pan device found"); + return 0; + } + }); + + TetheringObservable.setBluetoothTethering(false); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isBluetoothTetheringEnabled()); + } + */ + + @Test + public void updateWifiTetheringState_ignoreFailingWifiAPReflection_keepsOldValueTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenThrow(new NoSuchMethodException()); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(true, "192.168.43.0/24", "wlan0"); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isWifiTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_ignoreFailingWifiAPReflection_keepsOldValueFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenThrow(new NoSuchMethodException()); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(false, "", ""); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isWifiTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_WifiApReflectionWithoutException_changeValueToTrue() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(false, "", ""); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertTrue(observable.isWifiTetheringEnabled()); + } + + @Test + public void updateWifiTetheringState_WifiApReflectionWithoutException_changeValueToFalse() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(false); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + TetheringObservable.setWifiTethering(true, "", ""); + TetheringStateManager.getInstance().init(mockContext); + TetheringStateManager manager = TetheringStateManager.getInstance(); + assertFalse(observable.isWifiTetheringEnabled()); + } + + @Test + public void testGetWifiAddressRangee_keepsLastSeenAddressAndInterface() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + //WifiTethering was switched on + TetheringObservable.setWifiTethering(true, "192.168.40.0/24", "wlan0"); + + assertEquals("192.168.40.0/24", observable.getTetheringState().wifiAddress); + assertEquals("192.168.40.0/24", observable.getTetheringState().lastSeenWifiAddress); + assertEquals("wlan0", observable.getTetheringState().wifiInterface); + assertEquals("wlan0", observable.getTetheringState().lastSeenWifiInterface); + //Wifi tethering was switched off + TetheringObservable.setWifiTethering(true, "", ""); + assertEquals("", observable.getTetheringState().wifiAddress); + assertEquals("192.168.40.0/24", observable.getTetheringState().lastSeenWifiAddress); + assertEquals("", observable.getTetheringState().wifiInterface); + assertEquals("wlan0", observable.getTetheringState().lastSeenWifiInterface); + } + + @Test + public void testGetUsbAddressRange_keepsLastSeenAddressAndInterface() throws Exception { + WifiManagerWrapper mockWrapper = mock(WifiManagerWrapper.class); + when(mockWrapper.isWifiAPEnabled()).thenReturn(true); + PowerMockito.whenNew(WifiManagerWrapper.class).withAnyArguments().thenReturn(mockWrapper); + + //UsbTethering was switched on + TetheringObservable.setUsbTethering(true, "192.168.40.0/24", "rndis0"); + assertEquals("192.168.40.0/24", observable.getTetheringState().usbAddress); + assertEquals("192.168.40.0/24", observable.getTetheringState().lastSeenUsbAddress); + assertEquals("rndis0", observable.getTetheringState().usbInterface); + assertEquals("rndis0", observable.getTetheringState().lastSeenUsbInterface); + //UsbTethering tethering was switched off + TetheringObservable.setUsbTethering(true, "", ""); + assertEquals("", observable.getTetheringState().usbAddress); + assertEquals("192.168.40.0/24", observable.getTetheringState().lastSeenUsbAddress); + assertEquals("", observable.getTetheringState().usbInterface); + assertEquals("rndis0", observable.getTetheringState().lastSeenUsbInterface); + } + + +}
\ No newline at end of file diff --git a/go/src/github.com/OperatorFoundation/shapeshifter-dispatcher b/go/src/github.com/OperatorFoundation/shapeshifter-dispatcher -Subproject 1f11064c46adf504e3a00fe82f66b8bd64adadb +Subproject 7f7dd9fa3e11173dc5f54b9fca7e693cfe93cc8 |