Allow capturing multiple target apps

The target app option has been extended to allow the selection
of multiple apps.

Closes #146
This commit is contained in:
emanuele-f
2024-01-30 18:52:37 +01:00
parent 59d283b273
commit 8ac6428bf1
16 changed files with 430 additions and 112 deletions
+4
View File
@@ -110,6 +110,10 @@
<activity
android:name=".activities.MitmSetupWizard"
android:launchMode="singleTop" />
<activity
android:name=".activities.AppFilterActivity"
android:launchMode="singleTop"
android:parentActivityName=".activities.MainActivity" />
<service
android:name=".CaptureService"
@@ -86,6 +86,7 @@ import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
@@ -127,7 +128,7 @@ public class CaptureService extends VpnService implements Runnable {
private String dns_server;
private long last_bytes;
private int last_connections;
private int app_filter_uid;
private int[] mAppFilterUids;
private PcapDumper mDumper;
private ConnectionsRegister conn_reg;
private Uri mPcapUri;
@@ -269,7 +270,7 @@ public class CaptureService extends VpnService implements Runnable {
if(mSettings.readFromPcap()) {
// Disable incompatible settings
mSettings.dump_mode = Prefs.DumpMode.NONE;
mSettings.app_filter = "";
mSettings.app_filter.clear();
mSettings.socks5_enabled = false;
mSettings.tls_decryption = false;
mSettings.root_capture = false;
@@ -406,14 +407,29 @@ public class CaptureService extends VpnService implements Runnable {
mDecryptionList = null;
if ((mSettings.app_filter != null) && (!mSettings.app_filter.isEmpty())) {
try {
app_filter_uid = Utils.getPackageUid(getPackageManager(), mSettings.app_filter, 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
app_filter_uid = -1;
ArrayList<Integer> 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<String> 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);
@@ -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<String> 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<String> 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<String> 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();
}
}
}
@@ -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() {
@@ -14,19 +14,21 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* 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<String> 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<String, Drawable> 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<String, Drawable> 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<String> 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<String, Drawable> 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);
}
}
@@ -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<String> 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<String> getStringList(Intent intent, String key) {
List<String> 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;
}
@@ -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<String> 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<String> getStringSet(SharedPreferences p, String key) {
Set<String> 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);
+63 -10
View File
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2021 - Emanuele Faranda
* Copyright 2021-24 - Emanuele Faranda
*/
#include <sys/un.h>
@@ -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;
}
+36 -2
View File
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* 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;
+1 -1
View File
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2020-21 - Emanuele Faranda
* Copyright 2020-24 - Emanuele Faranda
*/
#include <inttypes.h>
+5 -2
View File
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* 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);
+1 -1
View File
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2021-23 - Emanuele Faranda
* Copyright 2021-24 - Emanuele Faranda
*/
/*
-1
View File
@@ -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;
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/show_hint"
android:title="@string/hint"
android:orderInCategory="5"
android:icon="@drawable/ic_info"
app:showAsAction="ifRoom" />
</menu>
+2
View File
@@ -501,4 +501,6 @@
<string name="always">Always</string>
<string name="for_connections_to_decrypt">Only for connections to decrypt</string>
<string name="decrypt_quic_notice">Decrypting QUIC is currently not supported. As a workaround, stop the capture and select the option to block QUIC in the PCAPdroid settings</string>
<string name="target_apps">Target apps</string>
<string name="target_apps_help">Select the applications to capture</string>
</resources>
+2 -2
View File
@@ -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