From 9eed9ed2217e022b43b329c3141e948bfd0e85d0 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Thu, 1 Dec 2022 00:42:41 +0100 Subject: [PATCH] Implement firewall whitelist mode When whitelist mode is enabled, all the connections are blocked unless an app is manually whitelisted. DNS resolution by netd is always allowed, since it's not possible to determine the requesting app. With this mode enabled, the firewall blocklist is still used and has priority. For example, if an app is whitelisted but a block rule exists for the given domain, the connection will be blocked. The app provides a default whitelist with some essential services. Closes #232 --- .../remote_capture/CaptureService.java | 18 +++++-- .../emanuelef/remote_capture/PCAPdroid.java | 26 ++++++++++ .../activities/FirewallActivity.java | 37 +++++++++++--- .../adapters/AppsStatsAdapter.java | 7 +++ .../fragments/AppsFragment.java | 28 ++++++++++- .../fragments/ConnectionsFragment.java | 24 +++++++++ .../fragments/EditListFragment.java | 3 ++ .../fragments/FirewallStatus.java | 50 +++++++++++++++++-- .../remote_capture/model/ListInfo.java | 7 +++ .../emanuelef/remote_capture/model/Prefs.java | 13 ++++- app/src/main/jni/core/jni_impl.c | 47 +++++++++++++++++ app/src/main/jni/core/pcapdroid.c | 24 ++++++++- app/src/main/jni/core/pcapdroid.h | 5 +- app/src/main/res/layout/app_item.xml | 9 ++++ app/src/main/res/menu/app_context_menu.xml | 8 +++ .../main/res/menu/connection_context_menu.xml | 9 ++++ app/src/main/res/menu/firewall_menu.xml | 7 +++ app/src/main/res/values/strings.xml | 5 ++ 18 files changed, 309 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java index 31ab3d21..60d0faba 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -142,7 +142,8 @@ public class CaptureService extends VpnService implements Runnable { private boolean mStopping; private Blacklists mBlacklists; private Blocklist mBlocklist; - private MatchList mWhitelist; + private MatchList mMalwareWhitelist; + private MatchList mFirewallWhitelist; private SparseArray mIfIndexToName; private boolean mSocks5Enabled; private String mSocks5Address; @@ -451,13 +452,14 @@ public class CaptureService extends VpnService implements Runnable { } } - mWhitelist = PCAPdroid.getInstance().getMalwareWhitelist(); + mMalwareWhitelist = PCAPdroid.getInstance().getMalwareWhitelist(); mBlacklists = PCAPdroid.getInstance().getBlacklists(); if(mMalwareDetectionEnabled && !mBlacklists.needsUpdate()) reloadBlacklists(); checkBlacklistsUpdates(); mBlocklist = PCAPdroid.getInstance().getBlocklist(); + mFirewallWhitelist = PCAPdroid.getInstance().getFirewallWhitelist(); mConnUpdateThread = new Thread(this::connUpdateWork, "UpdateListener"); mConnUpdateThread.start(); @@ -1302,6 +1304,7 @@ public class CaptureService extends VpnService implements Runnable { if(mMalwareDetectionEnabled) reloadMalwareWhitelist(); reloadBlocklist(); + reloadFirewallWhitelist(); } } @@ -1377,12 +1380,20 @@ public class CaptureService extends VpnService implements Runnable { reloadBlocklist(mBlocklist.toListDescriptor()); } + public void reloadFirewallWhitelist() { + if(!mBilling.isFirewallVisible()) + return; + + Log.i(TAG, "reloading firewall whitelist"); + reloadFirewallWhitelist(Prefs.isFirewallWhitelistMode(mPrefs) ? mFirewallWhitelist.toListDescriptor() : null); + } + public static void reloadMalwareWhitelist() { if((INSTANCE == null) || !INSTANCE.mMalwareDetectionEnabled) return; Log.i(TAG, "reloading malware whitelist"); - reloadMalwareWhitelist(INSTANCE.mWhitelist.toListDescriptor()); + reloadMalwareWhitelist(INSTANCE.mMalwareWhitelist.toListDescriptor()); } public static void setFirewallEnabled(boolean enabled) { @@ -1433,6 +1444,7 @@ public class CaptureService extends VpnService implements Runnable { private static native void setDnsServer(String server); private static native void reloadBlacklists(); private static native boolean reloadBlocklist(MatchList.ListDescriptor blocklist); + private static native boolean reloadFirewallWhitelist(MatchList.ListDescriptor whitelist); private static native boolean reloadMalwareWhitelist(MatchList.ListDescriptor whitelist); public static native void askStatsDump(); public static native byte[] getPcapHeader(); diff --git a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java index 38f608ad..dfda636e 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java +++ b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java @@ -48,6 +48,7 @@ public class PCAPdroid extends Application { private static final String TAG = "PCAPdroid"; private MatchList mVisMask; private MatchList mMalwareWhitelist; + private MatchList mFirewallWhitelist; private Blocklist mBlocklist; private Blacklists mBlacklists; private CtrlPermissions mCtrlPermissions; @@ -125,6 +126,31 @@ public class PCAPdroid extends Application { return mBlocklist; } + // use some safe defaults to guarantee basic services + private void initFirewallWhitelist() { + mFirewallWhitelist.addApp(0 /* root */); + mFirewallWhitelist.addApp(1000 /* android */); + mFirewallWhitelist.addApp(getPackageName() /* PCAPdroid */); + mFirewallWhitelist.addApp("com.google.android.gms" /* Google Play Services */); + mFirewallWhitelist.addApp("com.google.android.gsf" /* Google Services Framework (push notifications) */); + mFirewallWhitelist.addApp("com.google.android.ims" /* Carrier Services */); + mFirewallWhitelist.addApp("com.sec.spp.push" /* Samsung Push Service */); + mFirewallWhitelist.save(); + } + + public MatchList getFirewallWhitelist() { + if(mFirewallWhitelist == null) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mFirewallWhitelist = new MatchList(mLocalizedContext, Prefs.PREF_FIREWALL_WHITELIST); + + if(!Prefs.isFirewallWhitelistInitialized(prefs)) { + initFirewallWhitelist(); + Prefs.setFirewallWhitelistInitialized(prefs); + } + } + return mFirewallWhitelist; + } + public CtrlPermissions getCtrlPermissions() { if(mCtrlPermissions == null) mCtrlPermissions = new CtrlPermissions(this); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/FirewallActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/FirewallActivity.java index 5da556cb..76ea2cd5 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/FirewallActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/FirewallActivity.java @@ -19,11 +19,14 @@ package com.emanuelef.remote_capture.activities; +import android.annotation.SuppressLint; +import android.content.SharedPreferences; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceManager; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; @@ -32,15 +35,20 @@ import com.emanuelef.remote_capture.R; import com.emanuelef.remote_capture.fragments.EditListFragment; import com.emanuelef.remote_capture.fragments.FirewallStatus; import com.emanuelef.remote_capture.model.ListInfo; +import com.emanuelef.remote_capture.model.Prefs; import com.google.android.material.tabs.TabLayoutMediator; public class FirewallActivity extends BaseActivity { private static final String TAG = "Firewall"; private ViewPager2 mPager; + private StateAdapter mPagerAdapter; + private boolean mHasWhitelist = false; + private SharedPreferences mPrefs; private static final int POS_STATUS = 0; private static final int POS_BLOCKLIST = 1; - private static final int TOTAL_COUNT = 2; + private static final int POS_WHITELIST = 2; + private static final int TOTAL_COUNT = 3; @Override protected void onCreate(Bundle savedInstanceState) { @@ -48,11 +56,12 @@ public class FirewallActivity extends BaseActivity { setTitle(R.string.firewall); setContentView(R.layout.tabs_activity); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPager = findViewById(R.id.pager); setupTabs(); } - private static class StateAdapter extends FragmentStateAdapter { + private class StateAdapter extends FragmentStateAdapter { StateAdapter(final FragmentActivity fa) { super(fa); } @NonNull @@ -66,11 +75,13 @@ public class FirewallActivity extends BaseActivity { return new FirewallStatus(); case POS_BLOCKLIST: return EditListFragment.newInstance(ListInfo.Type.BLOCKLIST); + case POS_WHITELIST: + return EditListFragment.newInstance(ListInfo.Type.FIREWALL_WHITELIST); } } @Override - public int getItemCount() { return TOTAL_COUNT; } + public int getItemCount() { return mHasWhitelist ? TOTAL_COUNT : (TOTAL_COUNT - 1); } public int getPageTitle(final int position) { switch (position) { @@ -79,18 +90,32 @@ public class FirewallActivity extends BaseActivity { return R.string.status; case POS_BLOCKLIST: return R.string.blocklist; + case POS_WHITELIST: + return R.string.whitelist; } } } + @SuppressLint("NotifyDataSetChanged") + public void recheckTabs() { + boolean whitelist_mode = Prefs.isFirewallWhitelistMode(mPrefs); + if(mHasWhitelist == whitelist_mode) + return; + + mHasWhitelist = whitelist_mode; + mPagerAdapter.notifyDataSetChanged(); + } + private void setupTabs() { - final StateAdapter stateAdapter = new StateAdapter(this); - mPager.setAdapter(stateAdapter); + mPagerAdapter = new StateAdapter(this); + mPager.setAdapter(mPagerAdapter); new TabLayoutMediator(findViewById(R.id.tablayout), mPager, (tab, position) -> - tab.setText(getString(stateAdapter.getPageTitle(position))) + tab.setText(getString(mPagerAdapter.getPageTitle(position))) ).attach(); + recheckTabs(); + // TODO fix DPAD navigation on Android TV, see MainActivity.onKeyDown } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsStatsAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsStatsAdapter.java index 9fee995e..777435a8 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsStatsAdapter.java +++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsStatsAdapter.java @@ -39,6 +39,7 @@ import com.emanuelef.remote_capture.model.AppDescriptor; import com.emanuelef.remote_capture.model.AppStats; import com.emanuelef.remote_capture.AppsResolver; import com.emanuelef.remote_capture.model.Blocklist; +import com.emanuelef.remote_capture.model.MatchList; import java.util.ArrayList; import java.util.Collections; @@ -50,6 +51,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter mStats; @@ -59,6 +61,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter(); mFirewallAvailable = Billing.newInstance(context).isFirewallVisible(); diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java index 1390cdba..2f2a19ef 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java @@ -20,6 +20,7 @@ package com.emanuelef.remote_capture.fragments; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -34,6 +35,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; import com.emanuelef.remote_capture.CaptureService; import com.emanuelef.remote_capture.ConnectionsRegister; @@ -46,6 +48,8 @@ import com.emanuelef.remote_capture.interfaces.ConnectionsListener; import com.emanuelef.remote_capture.model.AppStats; import com.emanuelef.remote_capture.model.Blocklist; import com.emanuelef.remote_capture.model.ConnectionDescriptor; +import com.emanuelef.remote_capture.model.MatchList; +import com.emanuelef.remote_capture.model.Prefs; import com.emanuelef.remote_capture.views.EmptyRecyclerView; public class AppsFragment extends Fragment implements ConnectionsListener { @@ -126,9 +130,16 @@ public class AppsFragment extends Fragment implements ConnectionsListener { if(stats == null) return; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); boolean isBlocked = PCAPdroid.getInstance().getBlocklist().matchesApp(stats.getUid()); menu.findItem(R.id.block_app).setVisible(!isBlocked); + if(Prefs.isFirewallWhitelistMode(prefs)) { + boolean isWhitelisted = PCAPdroid.getInstance().getFirewallWhitelist().matchesApp(stats.getUid()); + menu.findItem(R.id.add_to_whitelist).setVisible(!isWhitelisted); + menu.findItem(R.id.remove_from_whitelist).setVisible(isWhitelisted); + } + menu.findItem(R.id.unblock_app_permanently).setVisible(isBlocked); menu.findItem(R.id.unblock_app_10m).setVisible(isBlocked) .setTitle(getString(R.string.unblock_for_n_minutes, 10)); @@ -142,6 +153,8 @@ public class AppsFragment extends Fragment implements ConnectionsListener { public boolean onContextItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); Blocklist blocklist = PCAPdroid.getInstance().getBlocklist(); + MatchList whitelist = PCAPdroid.getInstance().getFirewallWhitelist(); + boolean whitelistChanged = false; AppStats app = mAdapter.getSelectedItem(); if(id == R.id.block_app) @@ -154,10 +167,21 @@ public class AppsFragment extends Fragment implements ConnectionsListener { blocklist.unblockAppForMinutes(app.getUid(), 60); else if(id == R.id.unblock_app_8h) blocklist.unblockAppForMinutes(app.getUid(), 480); - else + else if(id == R.id.add_to_whitelist) { + whitelist.addApp(app.getUid()); + whitelistChanged = true; + } else if(id == R.id.remove_from_whitelist) { + whitelist.removeApp(app.getUid()); + whitelistChanged = true; + } else return super.onContextItemSelected(item); - blocklist.saveAndReload(); + if(whitelistChanged) { + whitelist.save(); + if (CaptureService.isServiceActive()) + CaptureService.requireInstance().reloadFirewallWhitelist(); + } else + blocklist.saveAndReload(); // refresh the item mAdapter.notifyItemChanged(app); diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java index 17398bb1..ced1d302 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java @@ -24,6 +24,7 @@ import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -47,6 +48,7 @@ import androidx.appcompat.widget.SearchView; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.RecyclerView; @@ -67,6 +69,7 @@ import com.emanuelef.remote_capture.adapters.ConnectionsAdapter; import com.emanuelef.remote_capture.model.FilterDescriptor; import com.emanuelef.remote_capture.model.MatchList; import com.emanuelef.remote_capture.model.MatchList.RuleType; +import com.emanuelef.remote_capture.model.Prefs; import com.emanuelef.remote_capture.views.EmptyRecyclerView; import com.emanuelef.remote_capture.interfaces.ConnectionsListener; import com.emanuelef.remote_capture.activities.EditFilterActivity; @@ -295,12 +298,15 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener MenuItem item; Billing billing = Billing.newInstance(ctx); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); boolean firewallVisible = billing.isFirewallVisible(); + boolean whitelistMode = Prefs.isFirewallWhitelistMode(prefs); boolean showPurchaseFirewall = (!billing.isPurchased(Billing.FIREWALL_SKU) && billing.isAvailable(Billing.FIREWALL_SKU)) && !CaptureService.isCapturingAsRoot(); boolean blockVisible = false; boolean unblockVisible = false; Blocklist blocklist = PCAPdroid.getInstance().getBlocklist(); + MatchList fwWhitelist = PCAPdroid.getInstance().getFirewallWhitelist(); if(app != null) { boolean appBlocked = blocklist.matchesApp(app.getUid()); @@ -333,6 +339,12 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener item.setTitle(label); item.setVisible(true); } + + if(firewallVisible && whitelistMode) { + boolean whitelisted = fwWhitelist.matchesApp(app.getUid()); + menu.findItem(R.id.add_to_whitelist).setVisible(!whitelisted); + menu.findItem(R.id.remove_from_whitelist).setVisible(whitelisted); + } } if((conn.info != null) && (!conn.info.isEmpty())) { @@ -442,11 +454,13 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener Context ctx = requireContext(); ConnectionDescriptor conn = mAdapter.getSelectedItem(); MatchList whitelist = PCAPdroid.getInstance().getMalwareWhitelist(); + MatchList fwWhitelist = PCAPdroid.getInstance().getFirewallWhitelist(); Blocklist blocklist = PCAPdroid.getInstance().getBlocklist(); boolean firewallPurchased = Billing.newInstance(ctx).isPurchased(Billing.FIREWALL_SKU); boolean mask_changed = false; boolean whitelist_changed = false; boolean blocklist_changed = false; + boolean firewall_wl_changed = false; if(conn == null) return super.onContextItemSelected(item); @@ -531,6 +545,12 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener } else if(id == R.id.unblock_domain) { blocklist.removeHost(Utils.getSecondLevelDomain(conn.info)); blocklist_changed = true; + } else if(id == R.id.add_to_whitelist) { + fwWhitelist.addApp(conn.uid); + firewall_wl_changed = true; + } else if(id == R.id.remove_from_whitelist) { + fwWhitelist.removeApp(conn.uid); + firewall_wl_changed = true; } else if(id == R.id.open_app_details) { Intent intent = new Intent(requireContext(), AppDetailsActivity.class); intent.putExtra(AppDetailsActivity.APP_UID_EXTRA, conn.uid); @@ -555,6 +575,10 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener } else if(whitelist_changed) { whitelist.save(); CaptureService.reloadMalwareWhitelist(); + } else if(firewall_wl_changed) { + fwWhitelist.save(); + if(CaptureService.isServiceActive()) + CaptureService.requireInstance().reloadFirewallWhitelist(); } else if(blocklist_changed) blocklist.saveAndReload(); diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java index 517b4c60..88b955eb 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java @@ -254,6 +254,9 @@ public class EditListFragment extends Fragment implements MatchList.ListChangeLi else if(mListInfo.getType() == ListInfo.Type.BLOCKLIST) { if(CaptureService.isServiceActive()) CaptureService.requireInstance().reloadBlocklist(); + } else if(mListInfo.getType() == ListInfo.Type.FIREWALL_WHITELIST) { + if(CaptureService.isServiceActive()) + CaptureService.requireInstance().reloadFirewallWhitelist(); } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/FirewallStatus.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/FirewallStatus.java index ebbc977c..55f4e5a9 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/FirewallStatus.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/FirewallStatus.java @@ -37,6 +37,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SwitchCompat; import androidx.core.content.ContextCompat; import androidx.core.view.MenuProvider; @@ -51,16 +52,20 @@ import com.emanuelef.remote_capture.PCAPdroid; import com.emanuelef.remote_capture.R; import com.emanuelef.remote_capture.Utils; import com.emanuelef.remote_capture.activities.ConnectionsActivity; +import com.emanuelef.remote_capture.activities.FirewallActivity; import com.emanuelef.remote_capture.activities.MainActivity; import com.emanuelef.remote_capture.model.Blocklist; import com.emanuelef.remote_capture.model.ConnectionDescriptor; import com.emanuelef.remote_capture.model.FilterDescriptor; +import com.emanuelef.remote_capture.model.MatchList; import com.emanuelef.remote_capture.model.Prefs; public class FirewallStatus extends Fragment implements MenuProvider { private static final String TAG = "FirewallStatus"; + private static boolean whitelistWarningAck = false; private Handler mHandler; private SharedPreferences mPrefs; + private Menu mMenu; private SwitchCompat mToggle; private ImageView mStatusIcon; private TextView mStatus; @@ -69,6 +74,7 @@ public class FirewallStatus extends Fragment implements MenuProvider { private TextView mNumRules; private TextView mLastBlock; private Blocklist mBlocklist; + private MatchList mWhitelist; private int mOkColor, mWarnColor, mGrayColor; @Override @@ -84,6 +90,7 @@ public class FirewallStatus extends Fragment implements MenuProvider { mPrefs = PreferenceManager.getDefaultSharedPreferences(ctx); mBlocklist = PCAPdroid.getInstance().getBlocklist(); + mWhitelist = PCAPdroid.getInstance().getFirewallWhitelist(); mStatus = view.findViewById(R.id.status); mHandler = new Handler(Looper.getMainLooper()); mStatusIcon = view.findViewById(R.id.status_icon); @@ -121,6 +128,7 @@ public class FirewallStatus extends Fragment implements MenuProvider { @Override public void onCreateMenu(@NonNull Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.firewall_menu, menu); + mMenu = menu; mToggle = (SwitchCompat) menu.findItem(R.id.toggle_btn).getActionView(); mToggle.setOnCheckedChangeListener((buttonView, isChecked) -> { @@ -135,9 +143,9 @@ public class FirewallStatus extends Fragment implements MenuProvider { updateStatus(); }); + menu.findItem(R.id.whitelist_mode).setChecked(Prefs.isFirewallWhitelistMode(mPrefs)); menu.findItem(R.id.block_new_apps).setChecked(Prefs.blockNewApps(mPrefs)); - - updateStatus(); + reloadMode(); } @Override @@ -153,16 +161,52 @@ public class FirewallStatus extends Fragment implements MenuProvider { item.setChecked(checked); mPrefs.edit().putBoolean(Prefs.PREF_BLOCK_NEW_APPS, checked).apply(); return true; + } else if(id == R.id.whitelist_mode) { + boolean checked = !item.isChecked(); + if(checked && !whitelistWarningAck) { + AlertDialog dialog = new AlertDialog.Builder(requireContext()) + .setTitle(R.string.whitelist_mode) + .setMessage(R.string.firewall_whitelist_notice) + .setPositiveButton(R.string.ok, (d, whichButton) -> { + whitelistWarningAck = true; + item.setChecked(true); + mPrefs.edit().putBoolean(Prefs.PREF_FIREWALL_WHITELIST_MODE, true).apply(); + reloadMode(); + }) + .setNegativeButton(R.string.cancel_action, (d, whichButton) -> {}) + .show(); + + dialog.setCanceledOnTouchOutside(false); + } else { + item.setChecked(checked); + mPrefs.edit().putBoolean(Prefs.PREF_FIREWALL_WHITELIST_MODE, checked).apply(); + reloadMode(); + } + return true; } return false; } + private void reloadMode() { + boolean whitelist_mode = Prefs.isFirewallWhitelistMode(mPrefs); + + if(mMenu != null) + mMenu.findItem(R.id.block_new_apps).setVisible(!whitelist_mode); + + if(CaptureService.isServiceActive()) + CaptureService.requireInstance().reloadFirewallWhitelist(); + + ((FirewallActivity)requireActivity()).recheckTabs(); + updateStatus(); + } + private void updateStatus() { Context ctx = requireContext(); ConnectionsRegister reg = CaptureService.getConnsRegister(); boolean is_running = CaptureService.isServiceActive(); boolean is_enabled = Prefs.isFirewallEnabled(ctx, mPrefs); + boolean whitelist_mode = Prefs.isFirewallWhitelistMode(mPrefs); if(!is_running) { mStatusIcon.setImageResource(R.drawable.ic_shield); @@ -190,7 +234,7 @@ public class FirewallStatus extends Fragment implements MenuProvider { mNumBlocked.setText(Utils.formatIntShort(((reg != null) ? reg.getNumBlockedConnections() : 0))); mNumChecked.setText(Utils.formatIntShort(CaptureService.getNumCheckedFirewallConnections())); mLastBlock.setText(Utils.formatEpochMin(ctx, ((reg != null) ? reg.getLastFirewallBlock() / 1000 : 0))); - mNumRules.setText(Utils.formatIntShort(mBlocklist.getSize())); + mNumRules.setText(Utils.formatIntShort(mBlocklist.getSize() + (whitelist_mode ? mWhitelist.getSize() : 0))); // Periodic update mHandler.postDelayed(this::updateStatus, 1000); diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/ListInfo.java b/app/src/main/java/com/emanuelef/remote_capture/model/ListInfo.java index cb6f7d60..fb09a999 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/ListInfo.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/ListInfo.java @@ -33,6 +33,7 @@ public class ListInfo { VISUALIZATION_MASK, MALWARE_WHITELIST, BLOCKLIST, + FIREWALL_WHITELIST, } public ListInfo(Type tp) { @@ -51,6 +52,8 @@ public class ListInfo { return PCAPdroid.getInstance().getMalwareWhitelist(); case BLOCKLIST: return PCAPdroid.getInstance().getBlocklist(); + case FIREWALL_WHITELIST: + return PCAPdroid.getInstance().getFirewallWhitelist(); } assert false; @@ -65,6 +68,8 @@ public class ListInfo { return R.string.malware_whitelist_rules; case BLOCKLIST: return R.string.firewall_rules; + case FIREWALL_WHITELIST: + return R.string.whitelist; } assert false; @@ -79,6 +84,8 @@ public class ListInfo { return R.string.malware_whitelist_help; case BLOCKLIST: return 0; + case FIREWALL_WHITELIST: + return R.string.firewall_whitelist_help; } assert false; diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java index 071bb543..10905b17 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java @@ -46,6 +46,9 @@ public class Prefs { public static final String PAYLOAD_MODE_FULL = "full"; public static final String DEFAULT_PAYLOAD_MODE = PAYLOAD_MODE_MINIMAL; + // used to initialize the whitelist with some safe defaults + public static final int FIREWALL_WHITELIST_INIT_VER = 1; + public static final String PREF_COLLECTOR_IP_KEY = "collector_ip_address"; public static final String PREF_COLLECTOR_PORT_KEY = "collector_port"; public static final String PREF_SOCKS5_PROXY_IP_KEY = "socks5_proxy_ip_address"; @@ -66,6 +69,9 @@ public class Prefs { public static final String PREF_MALWARE_WHITELIST = "malware_whitelist"; public static final String PREF_PCAPDROID_TRAILER = "pcapdroid_trailer"; public static final String PREF_BLOCKLIST = "bl"; + public static final String PREF_FIREWALL_WHITELIST_MODE = "firewall_wl_mode"; + public static final String PREF_FIREWALL_WHITELIST_INIT_VER = "firewall_wl_init"; + public static final String PREF_FIREWALL_WHITELIST = "firewall_whitelist"; public static final String PREF_START_AT_BOOT = "start_at_boot"; public static final String PREF_SNAPLEN = "snaplen"; public static final String PREF_MAX_PKTS_PER_FLOW = "max_pkts_per_flow"; @@ -140,6 +146,10 @@ public class Prefs { p.edit().putBoolean(PREF_LOCKDOWN_VPN_NOTICE_SHOWN, true).apply(); } + public static void setFirewallWhitelistInitialized(SharedPreferences p) { + p.edit().putInt(PREF_FIREWALL_WHITELIST_INIT_VER, FIREWALL_WHITELIST_INIT_VER).apply(); + } + /* Prefs with defaults */ public static String getCollectorIp(SharedPreferences p) { return(p.getString(PREF_COLLECTOR_IP_KEY, "127.0.0.1")); } public static int getCollectorPort(SharedPreferences p) { return(Integer.parseInt(p.getString(PREF_COLLECTOR_PORT_KEY, "1234"))); } @@ -172,9 +182,10 @@ public class Prefs { public static boolean isPrivateDnsBlockingEnabled(SharedPreferences p) { return(p.getBoolean(PREF_AUTO_BLOCK_PRIVATE_DNS, true)); } public static boolean lockdownVpnNoticeShown(SharedPreferences p) { return(p.getBoolean(PREF_LOCKDOWN_VPN_NOTICE_SHOWN, false)); } public static boolean blockNewApps(SharedPreferences p) { return(p.getBoolean(PREF_BLOCK_NEW_APPS, false)); } + public static boolean isFirewallWhitelistMode(SharedPreferences p) { return(p.getBoolean(PREF_FIREWALL_WHITELIST_MODE, false)); } + public static boolean isFirewallWhitelistInitialized(SharedPreferences p) { return(p.getInt(PREF_FIREWALL_WHITELIST_INIT_VER, 0) == FIREWALL_WHITELIST_INIT_VER); } public static String getMitmproxyOpts(SharedPreferences p) { return(p.getString(PREF_MITMPROXY_OPTS, "")); } - public static String asString(Context ctx) { SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(ctx); diff --git a/app/src/main/jni/core/jni_impl.c b/app/src/main/jni/core/jni_impl.c index 8adb24e9..ae930f54 100644 --- a/app/src/main/jni/core/jni_impl.c +++ b/app/src/main/jni/core/jni_impl.c @@ -779,6 +779,53 @@ Java_com_emanuelef_remote_1capture_CaptureService_reloadBlocklist(JNIEnv *env, j /* ******************************************************* */ +JNIEXPORT jboolean JNICALL +Java_com_emanuelef_remote_1capture_CaptureService_reloadFirewallWhitelist(JNIEnv *env, jclass clazz, + jobject whitelist) { + pcapdroid_t *pd = global_pd; + if(!pd) { + log_e("NULL pd instance"); + return false; + } + + if(pd->root_capture) { + log_e("firewall in root mode not implemented"); + return false; + } + + if(pd->firewall.new_wl != NULL) { + log_e("previous whitelist not loaded yet"); + return false; + } + + if(whitelist == NULL) { + pd->firewall.wl_enabled = false; + log_d("firewall whitelist is disabled"); + return true; + } + + blacklist_t *wl = blacklist_init(); + if(!wl) { + log_e("blacklist_init failed"); + return false; + } + + if(blacklist_load_list_descriptor(wl, env, whitelist) < 0) { + blacklist_destroy(wl); + return false; + } + + blacklists_stats_t stats; + blacklist_get_stats(wl, &stats); + log_d("reloadFirewallWhitelist: %d apps, %d domains, %d IPs", stats.num_apps, stats.num_domains, stats.num_ips); + + pd->firewall.new_wl = wl; + pd->firewall.wl_enabled = true; + return true; +} + +/* ******************************************************* */ + JNIEXPORT jboolean JNICALL Java_com_emanuelef_remote_1capture_CaptureService_reloadMalwareWhitelist(JNIEnv *env, jclass clazz, jobject whitelist) { diff --git a/app/src/main/jni/core/pcapdroid.c b/app/src/main/jni/core/pcapdroid.c index 228da8df..1666c148 100644 --- a/app/src/main/jni/core/pcapdroid.c +++ b/app/src/main/jni/core/pcapdroid.c @@ -374,8 +374,9 @@ pd_conn_t* pd_new_connection(pcapdroid_t *pd, const zdtun_5tuple_t *tuple, int u conn->data->uid = data->uid; - if(!conn->data->to_block && pd->firewall.enabled - && blacklist_match_uid(pd->firewall.bl, conn->data->uid)) + if(!conn->data->to_block && pd->firewall.enabled && ( + blacklist_match_uid(pd->firewall.bl, conn->data->uid) || + (pd->firewall.wl_enabled && pd->firewall.wl && !blacklist_match_uid(pd->firewall.wl, conn->data->uid)))) conn->data->netd_block_missed = true; zdtun_5tuple2str(&conn->tuple, buf, sizeof(buf)); @@ -439,6 +440,12 @@ pd_conn_t* pd_new_connection(pcapdroid_t *pd, const zdtun_5tuple_t *tuple, int u fw_num_checked_connections++; } + if(pd->firewall.enabled && pd->firewall.wl_enabled && pd->firewall.wl && !data->to_block && data->uid != UID_NETD) { + // whitelist mode: block any app unless it's explicitly whitelisted. + // The blocklist still has priority to determine if a connection should be blocked. + data->to_block = !blacklist_match_uid(pd->firewall.wl, data->uid); + } + return(data); } @@ -887,6 +894,8 @@ static int check_blocked_conn_cb(pcapdroid_t *pd, const zdtun_5tuple_t *tuple, p blacklist_match_ip(fw_bl, &dst_ip, tuple->ipver) || (data->info && data->info[0] && blacklist_match_domain(fw_bl, data->info)); } + if(pd->firewall.enabled && pd->firewall.wl_enabled && pd->firewall.wl && !data->to_block && data->uid != UID_NETD) + data->to_block = !blacklist_match_uid(pd->firewall.wl, data->uid); if(old_block != data->to_block) { data->update_type |= CONN_UPDATE_STATS; @@ -1030,6 +1039,13 @@ void pd_housekeeping(pcapdroid_t *pd) { pd->firewall.bl = pd->firewall.new_bl; pd->firewall.new_bl = NULL; iter_active_connections(pd, check_blocked_conn_cb); + } else if(pd->firewall.new_wl) { + // Load new whitelist + if(pd->firewall.wl) + blacklist_destroy(pd->firewall.wl); + pd->firewall.wl = pd->firewall.new_wl; + pd->firewall.new_wl = NULL; + iter_active_connections(pd, check_blocked_conn_cb); } } @@ -1213,6 +1229,10 @@ int pd_run(pcapdroid_t *pd) { blacklist_destroy(pd->firewall.bl); if(pd->firewall.new_bl) blacklist_destroy(pd->firewall.new_bl); + if(pd->firewall.wl) + blacklist_destroy(pd->firewall.wl); + if(pd->firewall.new_wl) + blacklist_destroy(pd->firewall.new_wl); if(pd->malware_detection.enabled) { if(pd->malware_detection.reload_in_progress) { diff --git a/app/src/main/jni/core/pcapdroid.h b/app/src/main/jni/core/pcapdroid.h index 789ab55c..e247893a 100644 --- a/app/src/main/jni/core/pcapdroid.h +++ b/app/src/main/jni/core/pcapdroid.h @@ -263,8 +263,11 @@ typedef struct pcapdroid { struct { bool enabled; - blacklist_t *bl; + blacklist_t *bl; // blocklist blacklist_t *new_bl; + bool wl_enabled; + blacklist_t *wl; // whitelist + blacklist_t *new_wl; } firewall; } pcapdroid_t; diff --git a/app/src/main/res/layout/app_item.xml b/app/src/main/res/layout/app_item.xml index 4588123d..98e71627 100644 --- a/app/src/main/res/layout/app_item.xml +++ b/app/src/main/res/layout/app_item.xml @@ -1,6 +1,7 @@ + + + + + + + + + Additional mitmproxy options Provide additional options for mitmproxy Error while starting the mitm addon. Check the log for details + Whitelist mode + In this mode all the connections will be blocked, unless manually whitelisted. You may miss push notifications without a proper whitelist + These rules specify which connections to allow. Blocklist rules have priority over whitelist rules + Add to whitelist + Remove from whitelist