diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9157e14..fc05e68b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -110,6 +110,10 @@
+
uids = new ArrayList<>();
+
+ for (String package_name: mSettings.app_filter) {
+ int uid;
+
+ try {
+ uid = Utils.getPackageUid(getPackageManager(), package_name, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ continue;
+ }
+
+ uids.add(uid);
}
+
+ // populate the array only with resolved UIDs
+ mAppFilterUids = new int[uids.size()];
+
+ int i = 0;
+ for (Integer uid: uids)
+ mAppFilterUids[i++] = uid;
} else
- app_filter_uid = -1;
+ mAppFilterUids = new int[0];
mMalwareDetectionEnabled = Prefs.isMalwareDetectionEnabled(this, mPrefs);
mFirewallEnabled = Prefs.isFirewallEnabled(this, mPrefs);
@@ -458,7 +474,8 @@ public class CaptureService extends VpnService implements Runnable {
// NOTE: the API requires a package name, however it is converted to a UID
// (see Vpn.java addUserToRanges). This means that vpn routing happens on a UID basis,
// not on a package-name basis!
- builder.addAllowedApplication(mSettings.app_filter);
+ for (String package_name: mSettings.app_filter)
+ builder.addAllowedApplication(package_name);
} catch (PackageManager.NameNotFoundException e) {
String msg = String.format(getResources().getString(R.string.app_not_found), mSettings.app_filter);
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
@@ -958,7 +975,7 @@ public class CaptureService extends VpnService implements Runnable {
return rv;
}
- public static String getAppFilter() {
+ public static Set getAppFilter() {
return((INSTANCE != null) ? INSTANCE.mSettings.app_filter : null);
}
@@ -1291,7 +1308,7 @@ public class CaptureService extends VpnService implements Runnable {
public int isPcapngEnabled() { return(mSettings.pcapng_format ? 1 : 0); }
- public int getAppFilterUid() { return(app_filter_uid); }
+ public int[] getAppFilterUids() { return(mAppFilterUids); }
public int getMitmAddonUid() {
return MitmAddon.getUid(this);
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/AppFilterActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/AppFilterActivity.java
new file mode 100644
index 00000000..d9c646db
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/AppFilterActivity.java
@@ -0,0 +1,142 @@
+package com.emanuelef.remote_capture.activities;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.MenuProvider;
+import androidx.preference.PreferenceManager;
+
+import com.emanuelef.remote_capture.Log;
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.Utils;
+import com.emanuelef.remote_capture.fragments.AppsToggles;
+import com.emanuelef.remote_capture.model.AppDescriptor;
+import com.emanuelef.remote_capture.model.Prefs;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class AppFilterActivity extends BaseActivity implements MenuProvider {
+ private static final String TAG = "AppFilterActivity";
+ private AppFilterFragment mFragment;
+
+ @Override
+ @SuppressWarnings("deprecation")
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.target_apps);
+ setContentView(R.layout.fragment_activity);
+ addMenuProvider(this);
+ displayBackAction();
+
+ if (savedInstanceState != null)
+ mFragment = (AppFilterFragment) getSupportFragmentManager().getFragment(savedInstanceState, "fragment");
+ if (mFragment == null)
+ mFragment = new AppFilterFragment();
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment, mFragment)
+ .commit();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, 0, 0);
+ else
+ overridePendingTransition(0, 0);
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ getSupportFragmentManager().putFragment(outState, "fragment", mFragment);
+ }
+
+ @Override
+ public void onCreateMenu(@NonNull Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.hint_menu, menu);
+ }
+
+ @Override
+ public boolean onMenuItemSelected(@NonNull MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.show_hint) {
+ Utils.showHelpDialog(this, R.string.target_apps_help);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onBackPressed() {
+ if(mFragment.onBackPressed())
+ return;
+
+ super.onBackPressed();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0);
+ else
+ overridePendingTransition(0, 0);
+ }
+
+ public static class AppFilterFragment extends AppsToggles {
+ private final Set mSelectedApps = new HashSet<>();
+ private @Nullable SharedPreferences mPrefs;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ assert mPrefs != null;
+
+ mSelectedApps.clear();
+
+ Set saved = Prefs.getStringSet(mPrefs, Prefs.PREF_APP_FILTER);
+ if(!saved.isEmpty()) {
+ Log.d(TAG, "Loading " + saved.size() + " target apps");
+ mSelectedApps.addAll(saved);
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mPrefs = null;
+ }
+
+ @Override
+ protected Set getCheckedApps() {
+ return mSelectedApps;
+ }
+
+ @Override
+ public void onAppToggled(AppDescriptor app, boolean checked) {
+ String packageName = app.getPackageName();
+ if(mSelectedApps.contains(packageName) == checked)
+ return; // nothing to do
+
+ if(checked)
+ mSelectedApps.add(packageName);
+ else
+ mSelectedApps.remove(packageName);
+
+ Log.d(TAG, "Saving " + mSelectedApps.size() + " target apps");
+
+ if(mPrefs == null)
+ return;
+
+ mPrefs.edit()
+ .putStringSet(Prefs.PREF_APP_FILTER, mSelectedApps)
+ .apply();
+ }
+ }
+}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/VpnExemptionsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/VpnExemptionsActivity.java
index de1fc69c..2a7c852f 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/VpnExemptionsActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/VpnExemptionsActivity.java
@@ -46,6 +46,7 @@ public class VpnExemptionsActivity extends BaseActivity {
super.onCreate(savedInstanceState);
setTitle(R.string.vpn_exemptions);
setContentView(R.layout.fragment_activity);
+ displayBackAction();
if(savedInstanceState != null)
mFragment = (VpnExceptionsFragment) getSupportFragmentManager().getFragment(savedInstanceState, "fragment");
@@ -63,15 +64,6 @@ public class VpnExemptionsActivity extends BaseActivity {
getSupportFragmentManager().putFragment(outState, "fragment", mFragment);
}
- @Override
- public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- if(item.getItemId() == android.R.id.home) {
- onBackPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
@Override
@SuppressWarnings("deprecation")
public void onBackPressed() {
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java
index 613b6198..2956a1b6 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java
@@ -14,19 +14,21 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2020-21 - Emanuele Faranda
+ * Copyright 2020-24 - Emanuele Faranda
*/
package com.emanuelef.remote_capture.fragments;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.method.LinkMovementMethod;
+import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -38,8 +40,8 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.content.ContextCompat;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
@@ -48,6 +50,7 @@ import androidx.preference.PreferenceManager;
import com.emanuelef.remote_capture.AppsResolver;
import com.emanuelef.remote_capture.Log;
import com.emanuelef.remote_capture.MitmReceiver;
+import com.emanuelef.remote_capture.activities.AppFilterActivity;
import com.emanuelef.remote_capture.model.AppDescriptor;
import com.emanuelef.remote_capture.model.AppState;
import com.emanuelef.remote_capture.CaptureService;
@@ -57,9 +60,11 @@ import com.emanuelef.remote_capture.activities.MainActivity;
import com.emanuelef.remote_capture.interfaces.AppStateListener;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.model.CaptureStats;
-import com.emanuelef.remote_capture.views.AppSelectDialog;
import com.emanuelef.remote_capture.views.PrefSpinner;
+import java.util.ArrayList;
+import java.util.Set;
+
public class StatusFragment extends Fragment implements AppStateListener, MenuProvider {
private static final String TAG = "StatusFragment";
private Handler mHandler;
@@ -75,9 +80,8 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
private SharedPreferences mPrefs;
private TextView mFilterDescription;
private SwitchCompat mAppFilterSwitch;
- private String mAppFilter;
+ private Set mAppFilter;
private TextView mFilterRootDecryptionWarning;
- private AppSelectDialog mAppSelDialog;
@Override
public void onAttach(@NonNull Context context) {
@@ -88,7 +92,6 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
@Override
public void onDetach() {
super.onDetach();
- abortAppSelection();
mActivity.setAppStateListener(null);
mActivity = null;
}
@@ -145,14 +148,11 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
});
}
- filterTitle.setText(R.string.app_filter);
+ filterTitle.setText(R.string.target_apps);
- mAppFilterSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if(isChecked) {
- if((mAppFilter == null) || (mAppFilter.isEmpty()))
- openAppFilterSelector();
- } else
- setAppFilter(null);
+ mAppFilterSwitch.setOnClickListener((buttonView) -> {
+ mAppFilterSwitch.setChecked(!mAppFilterSwitch.isChecked());
+ openAppFilterSelector();
});
refreshFilterInfo();
@@ -222,34 +222,20 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
mAppFilterSwitch.setChecked(true);
- AppDescriptor app = AppsResolver.resolveInstalledApp(context.getPackageManager(), mAppFilter, 0);
- String description;
+ Pair pair = getAppFilterTextAndIcon(context);
- if(app == null)
- description = mAppFilter;
- else {
- description = app.getName() + " (" + app.getPackageName() + ")";
+ mFilterDescription.setText(pair.first);
+
+ if (pair.second != null) {
int height = mFilterDescription.getMeasuredHeight();
- if((height > 0) && (app.getIcon() != null)) {
- Drawable drawable = Utils.scaleDrawable(context.getResources(), app.getIcon(), height, height);
+ if(height > 0) {
+ Drawable drawable = Utils.scaleDrawable(context.getResources(), pair.second, height, height);
if(drawable != null)
mFilterDescription.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
}
}
-
- mFilterDescription.setText(description);
- }
-
- private void setAppFilter(AppDescriptor filter) {
- SharedPreferences.Editor editor = mPrefs.edit();
- mAppFilter = (filter != null) ? filter.getPackageName() : "";
-
- editor.putString(Prefs.PREF_APP_FILTER, mAppFilter);
- editor.apply();
- refreshFilterInfo();
- recheckFilterWarning();
}
private void onStatsUpdate(CaptureStats stats) {
@@ -259,6 +245,42 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
mCaptureStatus.setText(Utils.formatBytes(stats.bytes_sent + stats.bytes_rcvd));
}
+ private Pair getAppFilterTextAndIcon(@NonNull Context context) {
+ Drawable icon = null;
+ String text = "";
+
+ if((mAppFilter != null) && (!mAppFilter.isEmpty())) {
+ if (mAppFilter.size() == 1) {
+ // only a single app is selected, show its image and text
+ String package_name = mAppFilter.iterator().next();
+ AppDescriptor app = AppsResolver.resolveInstalledApp(requireContext().getPackageManager(), package_name, 0);
+
+ if((app != null) && (app.getIcon() != null)) {
+ icon = app.getIcon();
+ text = app.getName() + " (" + app.getPackageName() + ")";
+ }
+ } else {
+ // multiple apps, show default icon and comprehensive text
+ icon = ContextCompat.getDrawable(context, R.drawable.ic_image);
+ ArrayList parts = new ArrayList<>();
+
+ for (String package_name: mAppFilter) {
+ AppDescriptor app = AppsResolver.resolveInstalledApp(requireContext().getPackageManager(), package_name, 0);
+ String tmp = package_name;
+
+ if (app != null)
+ tmp = app.getName();
+
+ parts.add(tmp);
+ }
+
+ text = Utils.shorten(String.join(", ", parts), 48);
+ }
+ }
+
+ return new Pair<>(text, icon);
+ }
+
private void refreshPcapDumpInfo() {
String info = "";
@@ -287,26 +309,29 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
mCollectorInfo.setText(info);
- // Check if a filter is set
- if((mAppFilter != null) && (!mAppFilter.isEmpty())) {
- AppDescriptor app = AppsResolver.resolveInstalledApp(requireContext().getPackageManager(), mAppFilter, 0);
+ // Rendering after mCollectorInfo.setText is deferred, so getMeasuredHeight must be postponed
+ mHandler.post(() -> {
+ Context context = getContext();
+ if(context == null)
+ return;
- if((app != null) && (app.getIcon() != null)) {
- // Rendering after mCollectorInfo.setText is deferred, so getMeasuredHeight must be postponed
- mHandler.post(() -> {
- if(getContext() == null)
- return;
+ // Check if a filter is set
+ if((mAppFilter != null) && (!mAppFilter.isEmpty())) {
+ Pair pair = getAppFilterTextAndIcon(context);
+ if (pair.second != null) {
+ // scale and set
int height = mCollectorInfo.getMeasuredHeight();
- Drawable drawable = Utils.scaleDrawable(getResources(), app.getIcon(), height, height);
+ Drawable drawable = Utils.scaleDrawable(getResources(), pair.second, height, height);
- if(drawable != null)
+ if (drawable != null)
mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
- });
- } else
- mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
- } else
- mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ }
+ }
+ });
+
+ // will be overriden in the above handler if necessary
+ mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
@Override
@@ -386,26 +411,7 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr
}
private void openAppFilterSelector() {
- mAppSelDialog = new AppSelectDialog((AppCompatActivity) requireActivity(), R.string.app_filter,
- new AppSelectDialog.AppSelectListener() {
- @Override
- public void onSelectedApp(AppDescriptor app) {
- abortAppSelection();
- setAppFilter(app);
- }
-
- @Override
- public void onAppSelectionAborted() {
- abortAppSelection();
- setAppFilter(null);
- }
- });
- }
-
- private void abortAppSelection() {
- if(mAppSelDialog != null) {
- mAppSelDialog.abort();
- mAppSelDialog = null;
- }
+ Intent intent = new Intent(requireContext(), AppFilterActivity.class);
+ startActivity(intent);
}
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java b/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java
index d46c01d4..198ed456 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java
@@ -8,10 +8,15 @@ import android.os.Bundle;
import com.emanuelef.remote_capture.Billing;
import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
public class CaptureSettings implements Serializable {
public Prefs.DumpMode dump_mode;
- public String app_filter;
+ public Set app_filter;
public String collector_address;
public int collector_port;
public int http_server_port;
@@ -62,7 +67,7 @@ public class CaptureSettings implements Serializable {
public CaptureSettings(Context ctx, Intent intent) {
dump_mode = Prefs.getDumpMode(getString(intent, "pcap_dump_mode", "none"));
- app_filter = getString(intent, Prefs.PREF_APP_FILTER, "");
+ app_filter = new HashSet<>(getStringList(intent, Prefs.PREF_APP_FILTER));
collector_address = getString(intent, Prefs.PREF_COLLECTOR_IP_KEY, "127.0.0.1");
collector_port = getInt(intent, Prefs.PREF_COLLECTOR_PORT_KEY, 1234);
http_server_port = getInt(intent, Prefs.PREF_HTTP_SERVER_PORT, 8080);
@@ -113,6 +118,25 @@ public class CaptureSettings implements Serializable {
return bundle.getBoolean(key, def_value);
}
+ // get a list of comma-separated strings from the bundle
+ private static List getStringList(Intent intent, String key) {
+ List rv;
+
+ String s = intent.getStringExtra(key);
+ if(s != null) {
+ if (s.indexOf(',') < 0) {
+ rv = new ArrayList<>();
+ rv.add(s);
+ } else {
+ String[] arr = s.split(",");
+ rv = Arrays.asList(arr);
+ }
+ } else
+ rv = new ArrayList<>();
+
+ return rv;
+ }
+
public boolean readFromPcap() {
return input_pcap_path != null;
}
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 b9b7712c..fc998ced 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
@@ -19,9 +19,11 @@
package com.emanuelef.remote_capture.model;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
+import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.emanuelef.remote_capture.Billing;
@@ -29,6 +31,9 @@ import com.emanuelef.remote_capture.BuildConfig;
import com.emanuelef.remote_capture.MitmAddon;
import com.emanuelef.remote_capture.Utils;
+import java.util.HashSet;
+import java.util.Set;
+
public class Prefs {
public static final String DUMP_NONE = "none";
public static final String DUMP_HTTP_SERVER = "http_server";
@@ -199,7 +204,7 @@ public class Prefs {
public static boolean isSocks5AuthEnabled(SharedPreferences p) { return(p.getBoolean(PREF_SOCKS5_AUTH_ENABLED_KEY, false)); }
public static String getSocks5Username(SharedPreferences p) { return(p.getString(PREF_SOCKS5_USERNAME_KEY, "")); }
public static String getSocks5Password(SharedPreferences p) { return(p.getString(PREF_SOCKS5_PASSWORD_KEY, "")); }
- public static String getAppFilter(SharedPreferences p) { return(p.getString(PREF_APP_FILTER, "")); }
+ public static Set getAppFilter(SharedPreferences p) { return(getStringSet(p, PREF_APP_FILTER)); }
public static IpMode getIPMode(SharedPreferences p) { return(getIPMode(p.getString(PREF_IP_MODE, IP_MODE_DEFAULT))); }
public static BlockQuicMode getBlockQuicMode(SharedPreferences p) { return(getBlockQuicMode(p.getString(PREF_BLOCK_QUIC, BLOCK_QUIC_MODE_DEFAULT))); }
public static boolean useEnglishLanguage(SharedPreferences p){ return("english".equals(p.getString(PREF_APP_LANGUAGE, "system")));}
@@ -234,6 +239,31 @@ public class Prefs {
public static String getDnsServerV4(SharedPreferences p) { return(p.getString(PREF_DNS_SERVER_V4, "1.1.1.1")); }
public static String getDnsServerV6(SharedPreferences p) { return(p.getString(PREF_DNS_SERVER_V6, "2606:4700:4700::1111")); }
+ // Gets a StringSet from the prefs
+ // The preference should either be a StringSet or a String
+ // An empty set is returned as the default value
+ @SuppressLint("MutatingSharedPrefs")
+ public static @NonNull Set getStringSet(SharedPreferences p, String key) {
+ Set rv = null;
+
+ try {
+ rv = p.getStringSet(key, null);
+ } catch (ClassCastException e) {
+ // retry with string
+ String s = p.getString(key, "");
+
+ if (!s.isEmpty()) {
+ rv = new HashSet<>();
+ rv.add(s);
+ }
+ }
+
+ if (rv == null)
+ rv = new HashSet<>();
+
+ return rv;
+ }
+
public static String asString(Context ctx) {
SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(ctx);
@@ -252,7 +282,7 @@ public class Prefs {
"\nFirewall: " + isFirewallEnabled(ctx, p) +
"\nPCAPNG: " + isPcapngEnabled(ctx, p) +
"\nBlockNewApps: " + blockNewApps(p) +
- "\nAppFilter: " + getAppFilter(p) +
+ "\nTargetApps: " + getAppFilter(p) +
"\nIpMode: " + getIPMode(p) +
"\nTrailer: " + isPcapdroidTrailerEnabled(p) +
"\nStartAtBoot: " + startAtBoot(p);
diff --git a/app/src/main/jni/core/capture_libpcap.c b/app/src/main/jni/core/capture_libpcap.c
index 178c682e..2aa89e79 100644
--- a/app/src/main/jni/core/capture_libpcap.c
+++ b/app/src/main/jni/core/capture_libpcap.c
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2021 - Emanuele Faranda
+ * Copyright 2021-24 - Emanuele Faranda
*/
#include
@@ -103,6 +103,25 @@ static bool valid_bpf(const char *bpf) {
/* ******************************************************* */
+static const char* get_uids_filter(pcapdroid_t *pd, char *buf, size_t buf_size) {
+ if (pd->tls_decryption.enabled || (pd->pcap.app_filter_uids_size <= 0))
+ return "-1";
+
+ size_t off = 0;
+
+ for (int i = 0; (i < pd->pcap.app_filter_uids_size) && (off < buf_size); i++) {
+ const char * fmt = (i == 0) ? "%d" : ",%d";
+ off += snprintf(buf + off, (buf_size - off), fmt, pd->pcap.app_filter_uids[i]);
+ }
+
+ if (off >= buf_size)
+ log_e("The UID filter has been truncated");
+
+ return buf;
+}
+
+/* ******************************************************* */
+
static int connectPcapd(pcapdroid_t *pd) {
int sock;
int client = -1;
@@ -185,9 +204,11 @@ static int connectPcapd(pcapdroid_t *pd) {
}
// Start the daemon
+ char uids_filter_buf[128];
char args[256];
- snprintf(args, sizeof(args), "-l pcapd.log -L %u -i '%s' -u %d -t -b '%s'%s",
- getuid(), pd->pcap.capture_interface, pd->tls_decryption.enabled ? -1 : pd->app_filter,
+ snprintf(args, sizeof(args), "-l pcapd.log -L %u -i '%s' -u %s -t -b '%s'%s",
+ getuid(), pd->pcap.capture_interface,
+ get_uids_filter(pd, uids_filter_buf, sizeof(uids_filter_buf)),
bpf, pd->pcap.daemonize ? " -d" : "");
pid = start_subprocess(pcapd, args, pd->pcap.as_root, NULL);
@@ -271,10 +292,10 @@ cleanup:
/* ******************************************************* */
-static char* get_mitm_redirection_args(pcapdroid_t *pd, char *buf, bool add) {
+static char* get_mitm_redirection_args(pcapdroid_t *pd, char *buf, int uid, bool add) {
int off = sprintf(buf, "-t nat -%c OUTPUT -p tcp -m owner ", add ? 'I' : 'D');
- if(pd->app_filter >= 0)
- off += sprintf(buf + off, "--uid-owner %d", pd->app_filter);
+ if(uid >= 0)
+ off += sprintf(buf + off, "--uid-owner %d", uid);
else
off += sprintf(buf + off, "! --uid-owner %d", pd->mitm_addon_uid);
sprintf(buf + off, " -j REDIRECT --to 7780");
@@ -604,6 +625,7 @@ int run_libpcap(pcapdroid_t *pd) {
char bpf[256];
bpf[0] = '\0';
+ pd->pcap.app_filter_uids_size = getIntArrayPref(pd->env, pd->capture_service, "getAppFilterUids", &pd->pcap.app_filter_uids);
pd->pcap.as_root = !pd->pcap_file_capture;
pd->pcap.bpf = getStringPref(pd, "getPcapDumperBpf", bpf, sizeof(bpf));
pd->pcap.capture_interface = getStringPref(pd, "getCaptureInterface", capture_interface, sizeof(capture_interface));
@@ -628,10 +650,23 @@ int run_libpcap(pcapdroid_t *pd) {
if(pd->tls_decryption.enabled) {
char args[128];
- if(run_shell_cmd("iptables", get_mitm_redirection_args(pd, args, true), true, true) != 0)
- goto cleanup;
+ if(pd->pcap.app_filter_uids_size > 0) {
+ for (int i = 0; i < pd->pcap.app_filter_uids_size; i++) {
+ int uid = pd->pcap.app_filter_uids[i];
- iptables_cleanup = true;
+ if (uid >= 0) {
+ if(run_shell_cmd("iptables", get_mitm_redirection_args(pd, args, uid, true), true, true) != 0)
+ goto cleanup;
+
+ iptables_cleanup = true;
+ }
+ }
+ } else {
+ if(run_shell_cmd("iptables", get_mitm_redirection_args(pd, args, -1, true), true, true) != 0)
+ goto cleanup;
+
+ iptables_cleanup = true;
+ }
}
pd_refresh_time(pd);
@@ -722,7 +757,16 @@ cleanup:
if(iptables_cleanup) {
char args[128];
- run_shell_cmd("iptables", get_mitm_redirection_args(pd, args, false), true, false);
+
+ if(pd->pcap.app_filter_uids_size > 0) {
+ for (int i = 0; i < pd->pcap.app_filter_uids_size; i++) {
+ int uid = pd->pcap.app_filter_uids[i];
+
+ if (uid >= 0)
+ run_shell_cmd("iptables", get_mitm_redirection_args(pd, args, uid, false), true, false);
+ }
+ } else
+ run_shell_cmd("iptables", get_mitm_redirection_args(pd, args, -1, false), true, false);
}
if((pd->pcap.pcapd_pid > 0) && !pd->pcap.daemonize) {
@@ -735,5 +779,14 @@ cleanup:
process_pcapd_rv(pd, WEXITSTATUS(status));
}
+#if ANDROID
+ if (pd->pcap.app_filter_uids) {
+ pd_free(pd->pcap.app_filter_uids);
+
+ pd->pcap.app_filter_uids = NULL;
+ pd->pcap.app_filter_uids_size = 0;
+ }
+#endif
+
return rv;
}
diff --git a/app/src/main/jni/core/jni_impl.c b/app/src/main/jni/core/jni_impl.c
index e7312b4d..76b5a37e 100644
--- a/app/src/main/jni/core/jni_impl.c
+++ b/app/src/main/jni/core/jni_impl.c
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2022 - Emanuele Faranda
+ * Copyright 2022-24 - Emanuele Faranda
*/
#if ANDROID
@@ -585,7 +585,6 @@ Java_com_emanuelef_remote_1capture_CaptureService_runPacketLoop(JNIEnv *env, jcl
.notify_blacklists_loaded = notifyBlacklistsLoaded,
.dump_payload_chunk = dumpPayloadChunk,
},
- .app_filter = getIntPref(env, vpn, "getAppFilterUid"),
.mitm_addon_uid = getIntPref(env, vpn, "getMitmAddonUid"),
.vpn_capture = (bool) getIntPref(env, vpn, "isVpnCapture"),
.pcap_file_capture = (bool) getIntPref(env, vpn, "isPcapFileCapture"),
@@ -1213,6 +1212,41 @@ int getIntPref(JNIEnv *env, jobject vpn_inst, const char *key) {
/* ******************************************************* */
+// Retrieve a int[] pref.
+// If rv is >0, out points to the allocated array. It's up to the caller to free it with pd_free
+int getIntArrayPref(JNIEnv *env, jobject vpn_inst, const char *key, int **out) {
+ int rv = -1;
+ jmethodID midMethod = jniGetMethodID(env, cls.vpn_service, key, "()[I");
+ jintArray jarr = (jintArray) (*env)->CallObjectMethod(env, vpn_inst, midMethod);
+
+ if (!jniCheckException(env)) {
+ int size = (*env)->GetArrayLength(env, jarr);
+ log_d("getIntArrayPref(%s) = #%d", key, size);
+
+ if (size > 0) {
+ jint *array = (*env)->GetIntArrayElements(env, jarr, NULL);
+ if (array) {
+ size_t arr_size = size * sizeof(int);
+
+ *out = (int*) pd_malloc(arr_size);
+ if (*out) {
+ // success
+ memcpy(*out, array, arr_size);
+ rv = size;
+ }
+
+ (*env)->ReleaseIntArrayElements(env, jarr, array, 0);
+ }
+ } else
+ rv = size;
+ }
+
+ (*env)->DeleteLocalRef(env, jarr);
+ return rv;
+}
+
+/* ******************************************************* */
+
void getApplicationByUid(pcapdroid_t *pd, jint uid, char *buf, int bufsize) {
JNIEnv *env = pd->env;
const char *value = NULL;
diff --git a/app/src/main/jni/core/pcapdroid.c b/app/src/main/jni/core/pcapdroid.c
index 29db9f6f..11495298 100644
--- a/app/src/main/jni/core/pcapdroid.c
+++ b/app/src/main/jni/core/pcapdroid.c
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2020-21 - Emanuele Faranda
+ * Copyright 2020-24 - Emanuele Faranda
*/
#include
diff --git a/app/src/main/jni/core/pcapdroid.h b/app/src/main/jni/core/pcapdroid.h
index c90badb0..72462915 100644
--- a/app/src/main/jni/core/pcapdroid.h
+++ b/app/src/main/jni/core/pcapdroid.h
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2020-21 - Emanuele Faranda
+ * Copyright 2020-24 - Emanuele Faranda
*/
#ifndef __PCAPDROID_H__
@@ -195,7 +195,6 @@ typedef struct pcapdroid {
int filesdir_len;
// config
- jint app_filter;
jint mitm_addon_uid;
bool vpn_capture;
bool pcap_file_capture;
@@ -233,6 +232,9 @@ typedef struct pcapdroid {
char *bpf;
char *capture_interface;
int pcapd_pid;
+
+ int *app_filter_uids;
+ int app_filter_uids_size;
} pcap;
};
@@ -406,6 +408,7 @@ uint16_t pd_ndpi2proto(ndpi_protocol proto);
char* getStringPref(pcapdroid_t *pd, const char *key, char *buf, int bufsize);
int getIntPref(JNIEnv *env, jobject vpn_inst, const char *key);
+int getIntArrayPref(JNIEnv *env, jobject vpn_inst, const char *key, int **out);
zdtun_ip_t getIPPref(JNIEnv *env, jobject vpn_inst, const char *key, int *ip_ver);
uint32_t getIPv4Pref(JNIEnv *env, jobject vpn_inst, const char *key);
struct in6_addr getIPv6Pref(JNIEnv *env, jobject vpn_inst, const char *key);
diff --git a/app/src/main/jni/pcapd/pcapd.c b/app/src/main/jni/pcapd/pcapd.c
index fc56699e..f3ab5f58 100644
--- a/app/src/main/jni/pcapd/pcapd.c
+++ b/app/src/main/jni/pcapd/pcapd.c
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2021-23 - Emanuele Faranda
+ * Copyright 2021-24 - Emanuele Faranda
*/
/*
diff --git a/app/src/main/jni/tests/test_utils.c b/app/src/main/jni/tests/test_utils.c
index 40de5c8d..c2bec727 100644
--- a/app/src/main/jni/tests/test_utils.c
+++ b/app/src/main/jni/tests/test_utils.c
@@ -89,7 +89,6 @@ pcapdroid_t* pd_init_test(const char *ifname) {
pd->pcap_file_capture = true;
pd->pcap.capture_interface = (char*) ifname;
pd->pcap.as_root = false; // don't run as root
- pd->app_filter = -1; // don't filter
pd->cb.get_libprog_path = getPcapdPath;
pd->payload_mode = PAYLOAD_MODE_FULL;
diff --git a/app/src/main/res/menu/hint_menu.xml b/app/src/main/res/menu/hint_menu.xml
new file mode 100644
index 00000000..9b0e9efa
--- /dev/null
+++ b/app/src/main/res/menu/hint_menu.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d292498c..ada18413 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -501,4 +501,6 @@
Always
Only for connections to decrypt
Decrypting QUIC is currently not supported. As a workaround, stop the capture and select the option to block QUIC in the PCAPdroid settings
+ Target apps
+ Select the applications to capture
diff --git a/docs/app_api.md b/docs/app_api.md
index d590b6ba..badc3df7 100644
--- a/docs/app_api.md
+++ b/docs/app_api.md
@@ -79,7 +79,7 @@ As shown above, the capture settings can be specified by using intent extras. Th
| Parameter | Type | Ver | Mode | Value |
|-------------------------|--------|-----|------|--------------------------------------------------------------------|
| pcap_dump_mode | string | | | none \| http_server \| udp_exporter \| pcap_file |
-| app_filter | string | | | the package name of the app to capture |
+| app_filter | string | | | package name of the app(s) to capture (73+: comma separated list) |
| collector_ip_address | string | | | the IP address of the collector in udp_exporter mode |
| collector_port | int | | | the UDP port of the collector in udp_exporter mode |
| http_server_port | int | | | the HTTP server port in http_server mode |
@@ -101,7 +101,7 @@ As shown above, the capture settings can be specified by using intent extras. Th
| pcapng_format | bool | 62 | | true to use the PCAPNG dump format (overrides pcapdroid_trailer)* |
| socks5_username | string | 64 | vpn | username for the optional SOCKS5 proxy authentication |
| socks5_password | string | 64 | vpn | password for the optional SOCKS5 proxy authentication |
-| block_quic | string | 73 | vpn | never | always | to_decrypt (matching the decryption whitelist) |
+| block_quic | string | 73 | vpn | never \| always \| to_decrypt (matching the decryption whitelist) |
\*: paid feature