summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorcyberta <cyberta@riseup.net>2020-01-31 22:46:24 -0800
committercyberta <cyberta@riseup.net>2020-01-31 22:46:24 -0800
commit0e8f40e75eb1a5fe2d3c212b5939fdbf427ec0f5 (patch)
treef6b0adef18755cc8c107897e625595614e5dce36 /app
parent721d222a457ec0dfec28bc4ee4908b50f04904fc (diff)
parentb8ba423d997f5dbb2541b4f4542a2b6b30400485 (diff)
Merge branch 'implement_tethering_for_rooted_devices' into 'master'
Implement tethering for rooted devices See merge request leap/bitmask_android!98
Diffstat (limited to 'app')
-rw-r--r--app/src/main/AndroidManifest.xml1
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java62
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Constants.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java89
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/firewall/FirewallCallback.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java151
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java220
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java71
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java94
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java88
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java239
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/TetheringBroadcastReceiver.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/TetheringObservable.java117
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/TetheringState.java45
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java181
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/WifiHotspotState.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tethering/WifiManagerWrapper.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/FirewallHelper.java196
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java51
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java86
-rw-r--r--app/src/main/res/drawable-hdpi/ic_access_point_36.pngbin0 -> 2246 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_bluetooth.pngbin0 -> 1043 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_cancel.pngbin0 -> 1780 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_check_bold.pngbin0 -> 808 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_usb.pngbin0 -> 1000 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_wifi.pngbin0 -> 1496 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_bluetooth.pngbin0 -> 797 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_cancel.pngbin0 -> 967 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_check_bold.pngbin0 -> 652 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_usb.pngbin0 -> 644 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_wifi.pngbin0 -> 881 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_access_point_36.pngbin0 -> 1301 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_bluetooth.pngbin0 -> 980 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_cancel.pngbin0 -> 1288 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_check_bold.pngbin0 -> 725 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_usb.pngbin0 -> 859 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_wifi.pngbin0 -> 1123 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_access_point_36.pngbin0 -> 2878 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_bluetooth.pngbin0 -> 1300 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_cancel.pngbin0 -> 2264 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_check_bold.pngbin0 -> 900 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_usb.pngbin0 -> 1397 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_wifi.pngbin0 -> 1894 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_bluetooth.pngbin0 -> 1805 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_cancel.pngbin0 -> 3290 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_check_bold.pngbin0 -> 1212 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_usb.pngbin0 -> 1707 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_wifi.pngbin0 -> 2639 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_access_point_36.pngbin0 -> 5906 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_bluetooth.pngbin0 -> 2138 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_cancel.pngbin0 -> 4551 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_check_bold.pngbin0 -> 1494 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_usb.pngbin0 -> 2359 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_wifi.pngbin0 -> 3399 bytes
-rw-r--r--app/src/main/res/layout/d_list_selection.xml51
-rw-r--r--app/src/main/res/layout/f_drawer_main.xml37
-rw-r--r--app/src/main/res/layout/v_icon_select_text_list_item.xml53
-rw-r--r--app/src/main/res/values/strings.xml11
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/tethering/TetheringStateManagerTest.java323
64 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
new file mode 100644
index 00000000..03444d0f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_access_point_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_bluetooth.png b/app/src/main/res/drawable-hdpi/ic_bluetooth.png
new file mode 100644
index 00000000..e7c1589b
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_bluetooth.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_cancel.png b/app/src/main/res/drawable-hdpi/ic_cancel.png
new file mode 100644
index 00000000..579b1dff
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_cancel.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_check_bold.png b/app/src/main/res/drawable-hdpi/ic_check_bold.png
new file mode 100644
index 00000000..28418346
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_check_bold.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_usb.png b/app/src/main/res/drawable-hdpi/ic_usb.png
new file mode 100644
index 00000000..b9de586f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_usb.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_wifi.png b/app/src/main/res/drawable-hdpi/ic_wifi.png
new file mode 100644
index 00000000..ca6b94a3
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_wifi.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_bluetooth.png b/app/src/main/res/drawable-ldpi/ic_bluetooth.png
new file mode 100644
index 00000000..3a73c82f
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_bluetooth.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_cancel.png b/app/src/main/res/drawable-ldpi/ic_cancel.png
new file mode 100644
index 00000000..a396f18e
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_cancel.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_check_bold.png b/app/src/main/res/drawable-ldpi/ic_check_bold.png
new file mode 100644
index 00000000..4f765ed4
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_check_bold.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_usb.png b/app/src/main/res/drawable-ldpi/ic_usb.png
new file mode 100644
index 00000000..d48d2f50
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_usb.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_wifi.png b/app/src/main/res/drawable-ldpi/ic_wifi.png
new file mode 100644
index 00000000..56ad6403
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_wifi.png
Binary files differ
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
new file mode 100644
index 00000000..c461a0a5
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_access_point_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_bluetooth.png b/app/src/main/res/drawable-mdpi/ic_bluetooth.png
new file mode 100644
index 00000000..a1cecd2b
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_bluetooth.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_cancel.png b/app/src/main/res/drawable-mdpi/ic_cancel.png
new file mode 100644
index 00000000..125c82f1
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_cancel.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_check_bold.png b/app/src/main/res/drawable-mdpi/ic_check_bold.png
new file mode 100644
index 00000000..872ef957
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_check_bold.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_usb.png b/app/src/main/res/drawable-mdpi/ic_usb.png
new file mode 100644
index 00000000..3ad5ebc1
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_usb.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_wifi.png b/app/src/main/res/drawable-mdpi/ic_wifi.png
new file mode 100644
index 00000000..21a69023
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_wifi.png
Binary files differ
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
new file mode 100644
index 00000000..4ae3d1d9
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_access_point_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_bluetooth.png b/app/src/main/res/drawable-xhdpi/ic_bluetooth.png
new file mode 100644
index 00000000..32a854e5
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_bluetooth.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_cancel.png b/app/src/main/res/drawable-xhdpi/ic_cancel.png
new file mode 100644
index 00000000..aac09d65
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_cancel.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_check_bold.png b/app/src/main/res/drawable-xhdpi/ic_check_bold.png
new file mode 100644
index 00000000..da6a1ecb
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_check_bold.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_usb.png b/app/src/main/res/drawable-xhdpi/ic_usb.png
new file mode 100644
index 00000000..c11940b1
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_usb.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_wifi.png b/app/src/main/res/drawable-xhdpi/ic_wifi.png
new file mode 100644
index 00000000..cf1bb909
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_wifi.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png b/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png
new file mode 100644
index 00000000..9c30e5b4
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_bluetooth.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxhdpi/ic_cancel.png
new file mode 100644
index 00000000..1e9bce6c
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_cancel.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_check_bold.png b/app/src/main/res/drawable-xxhdpi/ic_check_bold.png
new file mode 100644
index 00000000..f6b50706
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_check_bold.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_usb.png b/app/src/main/res/drawable-xxhdpi/ic_usb.png
new file mode 100644
index 00000000..c78e33af
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_usb.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_wifi.png b/app/src/main/res/drawable-xxhdpi/ic_wifi.png
new file mode 100644
index 00000000..ea9e08a9
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_wifi.png
Binary files differ
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
new file mode 100644
index 00000000..4a2f25c1
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_access_point_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png b/app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png
new file mode 100644
index 00000000..6eccbbd6
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_bluetooth.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxxhdpi/ic_cancel.png
new file mode 100644
index 00000000..4ef00efc
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_cancel.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_check_bold.png b/app/src/main/res/drawable-xxxhdpi/ic_check_bold.png
new file mode 100644
index 00000000..19029a0d
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_check_bold.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_usb.png b/app/src/main/res/drawable-xxxhdpi/ic_usb.png
new file mode 100644
index 00000000..4bebd840
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_usb.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_wifi.png b/app/src/main/res/drawable-xxxhdpi/ic_wifi.png
new file mode 100644
index 00000000..b5a05f7a
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_wifi.png
Binary files differ
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