mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-05-08 21:12:26 +00:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-9
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user