diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a60f405f..baee8082 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -81,6 +81,10 @@
android:name=".activities.VpnExemptionsActivity"
android:launchMode="singleTop"
android:parentActivityName=".activities.SettingsActivity" />
+
it = mappings.iter();
+ while (it.hasNext()) {
+ PortMappings.PortMap mapping = it.next();
+ addPortMapping(mapping.ipproto, mapping.orig_port, mapping.redirect_port, mapping.redirect_ip);
+ }
+ }
+
try {
mParcelFileDescriptor = builder.setSession(CaptureService.VpnSessionName).establish();
} catch (IllegalArgumentException | IllegalStateException e) {
@@ -1442,6 +1452,7 @@ public class CaptureService extends VpnService implements Runnable {
private static native int getFdSetSize();
private static native void setPrivateDnsBlocked(boolean to_block);
private static native void setDnsServer(String server);
+ private static native void addPortMapping(int ipproto, int orig_port, int redirect_port, String redirect_ip);
private static native void reloadBlacklists();
private static native boolean reloadBlocklist(MatchList.ListDescriptor blocklist);
private static native boolean reloadFirewallWhitelist(MatchList.ListDescriptor whitelist);
diff --git a/app/src/main/java/com/emanuelef/remote_capture/Utils.java b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
index c5f5a21d..8bfc66da 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
@@ -47,6 +47,7 @@ import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
+import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -65,6 +66,7 @@ import android.text.SpannedString;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.StyleSpan;
+import android.util.Patterns;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@@ -127,6 +129,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Random;
+import java.util.regex.Matcher;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -1428,4 +1431,23 @@ public class Utils {
else
return pm.getInstalledPackages(flags);
}
+
+ public static boolean validatePort(String value) {
+ try {
+ int val = Integer.parseInt(value);
+ return((val > 0) && (val < 65535));
+ } catch(NumberFormatException e) {
+ return false;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public static boolean validateIpAddress(String value) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ return (InetAddresses.isNumericAddress(value));
+ else {
+ Matcher matcher = Patterns.IP_ADDRESS.matcher(value);
+ return(matcher.matches());
+ }
+ }
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/PortMapActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/PortMapActivity.java
new file mode 100644
index 00000000..9571faea
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/PortMapActivity.java
@@ -0,0 +1,38 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2020-22 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.activities;
+
+import android.os.Bundle;
+
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.fragments.PortMapFragment;
+
+public class PortMapActivity extends BaseActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.port_mapping);
+ setContentView(R.layout.fragment_activity);
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment, new PortMapFragment())
+ .commit();
+ }
+}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java
index a753651c..7a3af726 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java
@@ -22,11 +22,8 @@ package com.emanuelef.remote_capture.activities;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.net.InetAddresses;
-import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
-import android.util.Patterns;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -55,7 +52,6 @@ import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
-import java.util.regex.Matcher;
public class SettingsActivity extends BaseActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, FragmentManager.OnBackStackChangedListener {
private static final String TAG = "SettingsActivity";
@@ -135,6 +131,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
private DropDownPreference mIpMode;
private DropDownPreference mCapInterface;
private Preference mVpnExceptions;
+ private Preference mPortMapping;
private Preference mMitmWizard;
private SwitchPreference mMalwareDetectionEnabled;
private Billing mIab;
@@ -188,41 +185,22 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
return pref;
}
- private boolean validatePort(String value) {
- try {
- int val = Integer.parseInt(value);
- return((val > 0) && (val < 65535));
- } catch(NumberFormatException e) {
- return false;
- }
- }
-
- @SuppressWarnings("deprecation")
- private boolean validateIp(String value) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- return (InetAddresses.isNumericAddress(value));
- else {
- Matcher matcher = Patterns.IP_ADDRESS.matcher(value);
- return(matcher.matches());
- }
- }
-
@SuppressWarnings("deprecation")
private void setupUdpExporterPrefs() {
/* Collector IP validation */
EditTextPreference mRemoteCollectorIp = requirePreference(Prefs.PREF_COLLECTOR_IP_KEY);
- mRemoteCollectorIp.setOnPreferenceChangeListener((preference, newValue) -> validateIp(newValue.toString()));
+ mRemoteCollectorIp.setOnPreferenceChangeListener((preference, newValue) -> Utils.validateIpAddress(newValue.toString()));
/* Collector port validation */
EditTextPreference mRemoteCollectorPort = requirePreference(Prefs.PREF_COLLECTOR_PORT_KEY);
mRemoteCollectorPort.setOnBindEditTextListener(editText -> editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED));
- mRemoteCollectorPort.setOnPreferenceChangeListener((preference, newValue) -> validatePort(newValue.toString()));
+ mRemoteCollectorPort.setOnPreferenceChangeListener((preference, newValue) -> Utils.validatePort(newValue.toString()));
}
private void setupHttpServerPrefs() {
/* HTTP Server port validation */
EditTextPreference mHttpServerPort = requirePreference(Prefs.PREF_HTTP_SERVER_PORT);
- mHttpServerPort.setOnPreferenceChangeListener((preference, newValue) -> validatePort(newValue.toString()));
+ mHttpServerPort.setOnPreferenceChangeListener((preference, newValue) -> Utils.validatePort(newValue.toString()));
}
private boolean rootCaptureEnabled() {
@@ -261,6 +239,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
private void setupCapturePrefs() {
mCapInterface = requirePreference(Prefs.PREF_CAPTURE_INTERFACE);
mVpnExceptions = requirePreference(Prefs.PREF_VPN_EXCEPTIONS);
+ mPortMapping = requirePreference(Prefs.PREF_PORT_MAPPING);
refreshInterfaces();
mVpnExceptions.setOnPreferenceClickListener(preference -> {
@@ -268,6 +247,12 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
startActivity(intent);
return true;
});
+
+ mPortMapping.setOnPreferenceClickListener(preference -> {
+ Intent intent = new Intent(requireContext(), PortMapActivity.class);
+ startActivity(intent);
+ return true;
+ });
}
private void setupSecurityPrefs() {
@@ -329,19 +314,12 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
/* TLS Proxy IP validation */
mSocks5ProxyIp = requirePreference(Prefs.PREF_SOCKS5_PROXY_IP_KEY);
- mSocks5ProxyIp.setOnPreferenceChangeListener((preference, newValue) -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- return (InetAddresses.isNumericAddress(newValue.toString()));
- else {
- Matcher matcher = Patterns.IP_ADDRESS.matcher(newValue.toString());
- return(matcher.matches());
- }
- });
+ mSocks5ProxyIp.setOnPreferenceChangeListener((preference, newValue) -> Utils.validateIpAddress(newValue.toString()));
/* TLS Proxy port validation */
mSocks5ProxyPort = requirePreference(Prefs.PREF_SOCKS5_PROXY_PORT_KEY);
mSocks5ProxyPort.setOnBindEditTextListener(editText -> editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED));
- mSocks5ProxyPort.setOnPreferenceChangeListener((preference, newValue) -> validatePort(newValue.toString()));
+ mSocks5ProxyPort.setOnPreferenceChangeListener((preference, newValue) -> Utils.validatePort(newValue.toString()));
}
private void fullPayloadHideShow(boolean tlsDecryption) {
@@ -428,6 +406,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
mIpMode.setVisible(!enabled);
mCapInterface.setVisible(enabled);
mVpnExceptions.setVisible(!enabled);
+ mPortMapping.setVisible(!enabled);
}
private boolean checkDecrpytionWithRoot(boolean rootEnabled, boolean tlsDecryption) {
diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/PortMappingAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/PortMappingAdapter.java
new file mode 100644
index 00000000..92d9f059
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/PortMappingAdapter.java
@@ -0,0 +1,78 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2020-22 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.adapters;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.Utils;
+import com.emanuelef.remote_capture.model.PortMappings;
+import com.emanuelef.remote_capture.model.PortMappings.PortMap;
+
+import java.util.Iterator;
+
+public class PortMappingAdapter extends ArrayAdapter {
+ private final LayoutInflater mLayoutInflater;
+ private final PortMappings mMappings;
+
+ public PortMappingAdapter(Context context, PortMappings mappings) {
+ super(context, R.layout.port_mapping_item);
+ mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mMappings = mappings;
+ reload();
+ }
+
+ @SuppressLint("SetTextI18n")
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ if(convertView == null)
+ convertView = mLayoutInflater.inflate(R.layout.port_mapping_item, parent, false);
+
+ PortMap mapping = getItem(position);
+ String redirect_to = getContext().getString(R.string.ip_and_port, mapping.redirect_ip, mapping.redirect_port);
+
+ ((TextView)convertView.findViewById(R.id.orig_port)).setText(Integer.toString(mapping.orig_port));
+ ((TextView)convertView.findViewById(R.id.proto)).setText(Utils.proto2str(mapping.ipproto));
+ ((TextView)convertView.findViewById(R.id.redirect_to)).setText(redirect_to);
+
+ return convertView;
+ }
+
+ public void reload() {
+ clear();
+
+ Iterator iterator = mMappings.iter();
+
+ while(iterator.hasNext()) {
+ PortMap item = iterator.next();
+ add(item);
+ }
+ }
+}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java
index 88b955eb..4cde41e2 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java
@@ -184,7 +184,7 @@ public class EditListFragment extends Fragment implements MatchList.ListChangeLi
} else {
for(MatchList.Rule item : mSelected)
mAdapter.remove(item);
- updateList();
+ updateListFromAdapter();
}
mode.finish();
@@ -260,7 +260,7 @@ public class EditListFragment extends Fragment implements MatchList.ListChangeLi
}
}
- private void updateList() {
+ private void updateListFromAdapter() {
ArrayList toRemove = new ArrayList<>();
Iterator iter = mList.iterRules();
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/PortMapFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/PortMapFragment.java
new file mode 100644
index 00000000..c686ca58
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/PortMapFragment.java
@@ -0,0 +1,299 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2020-22 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.fragments;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.view.MenuProvider;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+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.adapters.PortMappingAdapter;
+import com.emanuelef.remote_capture.model.PortMappings;
+import com.emanuelef.remote_capture.model.PortMappings.PortMap;
+import com.emanuelef.remote_capture.model.Prefs;
+import com.google.android.material.textfield.TextInputEditText;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Objects;
+
+
+public class PortMapFragment extends Fragment implements MenuProvider {
+ private static final String TAG = "PortMapFragment";
+ private PortMappingAdapter mAdapter;
+ private TextView mEmptyText;
+ private ListView mListView;
+ private PortMappings mMappings;
+ private ArrayList mSelected = new ArrayList<>();
+
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
+ return inflater.inflate(R.layout.simple_list, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ mListView = view.findViewById(R.id.listview);
+ mEmptyText = view.findViewById(R.id.list_empty);
+ mMappings = new PortMappings(requireContext());
+
+ mAdapter = new PortMappingAdapter(requireContext(), mMappings);
+ mListView.setAdapter(mAdapter);
+ mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
+ mListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ PortMap item = mAdapter.getItem(position);
+
+ if(checked)
+ mSelected.add(item);
+ else
+ mSelected.remove(item);
+
+ mode.setTitle(getString(R.string.n_selected, mSelected.size()));
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = requireActivity().getMenuInflater();
+ inflater.inflate(R.menu.list_edit_cab, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
+ int id = menuItem.getItemId();
+
+ if(id == R.id.delete_entry) {
+ confirmDelete(mode);
+ return true;
+ } else if(id == R.id.select_all) {
+ if(mSelected.size() >= mAdapter.getCount())
+ mode.finish();
+ else {
+ for(int i=0; i();
+ }
+ });
+
+ recheckListSize();
+ }
+
+ private void recheckListSize() {
+ mEmptyText.setVisibility((mAdapter.getCount() == 0) ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onCreateMenu(@NonNull Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.port_mapping_menu, menu);
+
+ SwitchCompat toggle = (SwitchCompat) menu.findItem(R.id.toggle_btn).getActionView();
+ toggle.setChecked(Prefs.isPortMappingEnabled(PreferenceManager.getDefaultSharedPreferences(requireContext())));
+ toggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
+
+ if(isChecked == Prefs.isPortMappingEnabled(prefs))
+ return; // not changed
+
+ Log.d(TAG, "Port mapping is now " + (isChecked ? "enabled" : "disabled"));
+ Prefs.setPortMappingEnabled(prefs, isChecked);
+ });
+ }
+
+ @Override
+ public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
+ if(menuItem.getItemId() == R.id.add_mapping) {
+ openAddDialog();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void openAddDialog() {
+ Context ctx = requireContext();
+ LayoutInflater inflater = LayoutInflater.from(ctx);
+
+ View view = inflater.inflate(R.layout.add_port_mapping_dialog, null);
+
+ final String[] protocols = {"TCP", "UDP"};
+ ArrayAdapter adapter = new ArrayAdapter<>(ctx, R.layout.dropdown_item, protocols);
+ AutoCompleteTextView protoField = (AutoCompleteTextView) view.findViewById(R.id.proto);
+ protoField.setText(protocols[0]);
+ protoField.setAdapter(adapter);
+
+ ((TextInputEditText) view.findViewById(R.id.redirect_ip)).setText("127.0.0.1");
+
+ AlertDialog dialog = new AlertDialog.Builder(ctx)
+ .setView(view)
+ .setTitle(R.string.port_mapping)
+ .setPositiveButton(R.string.add_action, (dialogInterface, i) -> {})
+ .setNegativeButton(R.string.cancel_action, (dialogInterface, i) -> {})
+ .show();
+ dialog.setCanceledOnTouchOutside(false);
+
+ // custom dismiss logic
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE)
+ .setOnClickListener(v -> {
+ PortMap mapping = validateAddDialog(view);
+ if(mapping == null)
+ return;
+
+ boolean exists = !mMappings.add(mapping);
+ if(exists)
+ Utils.showToastLong(requireContext(), R.string.port_mapping_exists);
+ else {
+ mMappings.save();
+ mAdapter.add(mapping);
+ recheckListSize();
+ }
+
+ dialog.dismiss();
+ });
+ }
+
+ private PortMap validateAddDialog(View view) {
+ TextInputEditText origPortField = (TextInputEditText) view.findViewById(R.id.orig_port);
+ TextInputEditText redirectIpField = (TextInputEditText) view.findViewById(R.id.redirect_ip);
+ TextInputEditText redirectPortField = (TextInputEditText) view.findViewById(R.id.redirect_port);
+
+ String origPort = Objects.requireNonNull(origPortField.getText()).toString();
+ String redirectIp = Objects.requireNonNull(redirectIpField.getText()).toString();
+ String redirectPort = Objects.requireNonNull(redirectPortField.getText()).toString();
+ String proto = ((AutoCompleteTextView) view.findViewById(R.id.proto)).getText().toString();
+
+ if(origPort.isEmpty()) {
+ origPortField.setError(getString(R.string.required));
+ return null;
+ }
+ if(!Utils.validatePort(origPort)) {
+ origPortField.setError(getString(R.string.invalid));
+ return null;
+ }
+
+ if(redirectIp.isEmpty()) {
+ redirectIpField.setError(getString(R.string.required));
+ return null;
+ }
+ if(!Utils.validateIpAddress(redirectIp)) {
+ redirectIpField.setError(getString(R.string.invalid));
+ return null;
+ }
+
+ if(redirectPort.isEmpty()) {
+ redirectPortField.setError(getString(R.string.required));
+ return null;
+ }
+ if(!Utils.validatePort(redirectPort)) {
+ redirectPortField.setError(getString(R.string.invalid));
+ return null;
+ }
+
+ return new PortMap(
+ proto.equals("TCP") ? 6 : 17, Integer.parseInt(origPort),
+ Integer.parseInt(redirectPort), redirectIp);
+ }
+
+ private void confirmDelete(ActionMode mode) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
+ builder.setMessage(R.string.items_delete_confirm);
+ builder.setCancelable(true);
+ builder.setPositiveButton(R.string.yes, (dialog, which) -> {
+ if(mSelected.size() >= mAdapter.getCount()) {
+ mAdapter.clear();
+ mMappings.clear();
+ mMappings.save();
+ } else {
+ for(PortMap item : mSelected)
+ mAdapter.remove(item);
+ updateMappingsFromAdapter();
+ }
+
+ mode.finish();
+ recheckListSize();
+ });
+ builder.setNegativeButton(R.string.no, (dialog, whichButton) -> {});
+
+ final AlertDialog alert = builder.create();
+ alert.setCanceledOnTouchOutside(true);
+ alert.show();
+ }
+
+ private void updateMappingsFromAdapter() {
+ ArrayList toRemove = new ArrayList<>();
+ Iterator iter = mMappings.iter();
+
+ // Remove the mList rules which are not in the adapter dataset
+ while(iter.hasNext()) {
+ PortMap mapping = iter.next();
+
+ if (mAdapter.getPosition(mapping) < 0)
+ toRemove.add(mapping);
+ }
+
+ if(toRemove.size() > 0) {
+ for(PortMap mapping: toRemove)
+ mMappings.remove(mapping);
+ mMappings.save();
+ }
+ }
+}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/PortMappings.java b/app/src/main/java/com/emanuelef/remote_capture/model/PortMappings.java
new file mode 100644
index 00000000..1f3aa3cf
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/model/PortMappings.java
@@ -0,0 +1,114 @@
+package com.emanuelef.remote_capture.model;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.preference.PreferenceManager;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.reflect.TypeToken;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Objects;
+
+public class PortMappings {
+ private static final String TAG = "PortMappings";
+ private final SharedPreferences mPrefs;
+ private ArrayList mMappings = new ArrayList<>();
+
+ public static class PortMap {
+ public final int ipproto;
+ public final int orig_port;
+ public final int redirect_port;
+ public final String redirect_ip;
+
+ public PortMap(int proto, int port, int r_port, String r_host) {
+ ipproto = proto;
+ orig_port = port;
+ redirect_port = r_port;
+ redirect_ip = r_host;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PortMap portMap = (PortMap) o;
+ return ipproto == portMap.ipproto && orig_port == portMap.orig_port &&
+ redirect_port == portMap.redirect_port && redirect_ip.equals(portMap.redirect_ip);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(ipproto, orig_port, redirect_port, redirect_ip);
+ }
+ }
+
+ public PortMappings(Context ctx) {
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ reload();
+ }
+
+ public void clear() {
+ mMappings.clear();
+ }
+
+ public void save() {
+ mPrefs.edit()
+ .putString(Prefs.PREF_PORT_MAPPING, toJson(false))
+ .apply();
+ }
+
+ public void reload() {
+ String serialized = mPrefs.getString(Prefs.PREF_PORT_MAPPING, "");
+ if(!serialized.isEmpty())
+ fromJson(serialized);
+ else
+ clear();
+ }
+
+ public boolean fromJson(String json_str) {
+ try {
+ Type listOfMyClassObject = new TypeToken>() {}.getType();
+ Gson gson = new Gson();
+ mMappings = gson.fromJson(json_str, listOfMyClassObject);
+ return true;
+ } catch (JsonParseException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public String toJson(boolean pretty_print) {
+ GsonBuilder builder = new GsonBuilder();
+ if(pretty_print)
+ builder.setPrettyPrinting();
+ Gson gson = builder.create();
+
+ String serialized = gson.toJson(mMappings);
+ //Log.d(TAG, "toJson: " + serialized);
+
+ return serialized;
+ }
+
+ // returns false if the mapping already exists
+ public boolean add(PortMap mapping) {
+ if(mMappings.contains(mapping))
+ return false;
+
+ mMappings.add(mapping);
+ return true;
+ }
+
+ public boolean remove(PortMap mapping) {
+ return mMappings.remove(mapping);
+ }
+
+ public Iterator iter() {
+ return mMappings.iterator();
+ }
+}
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 10905b17..8cb11ae8 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
@@ -85,6 +85,8 @@ public class Prefs {
public static final String PREF_APP_VERSION = "appver";
public static final String PREF_LOCKDOWN_VPN_NOTICE_SHOWN = "vpn_lockdown_notice";
public static final String PREF_VPN_EXCEPTIONS = "vpn_exceptions";
+ public static final String PREF_PORT_MAPPING = "port_mapping";
+ public static final String PREF_PORT_MAPPING_ENABLED = "port_mapping_enabled";
public static final String PREF_BLOCK_NEW_APPS = "block_new_apps";
public static final String PREF_PAYLOAD_NOTICE_ACK = "payload_notice";
public static final String PREF_REMOTE_COLLECTOR_ACK = "remote_collector_notice";
@@ -150,6 +152,10 @@ public class Prefs {
p.edit().putInt(PREF_FIREWALL_WHITELIST_INIT_VER, FIREWALL_WHITELIST_INIT_VER).apply();
}
+ public static void setPortMappingEnabled(SharedPreferences p, boolean enabled) {
+ p.edit().putBoolean(PREF_PORT_MAPPING_ENABLED, enabled).apply();
+ }
+
/* Prefs with defaults */
public static String getCollectorIp(SharedPreferences p) { return(p.getString(PREF_COLLECTOR_IP_KEY, "127.0.0.1")); }
public static int getCollectorPort(SharedPreferences p) { return(Integer.parseInt(p.getString(PREF_COLLECTOR_PORT_KEY, "1234"))); }
@@ -185,6 +191,7 @@ public class Prefs {
public static boolean isFirewallWhitelistMode(SharedPreferences p) { return(p.getBoolean(PREF_FIREWALL_WHITELIST_MODE, false)); }
public static boolean isFirewallWhitelistInitialized(SharedPreferences p) { return(p.getInt(PREF_FIREWALL_WHITELIST_INIT_VER, 0) == FIREWALL_WHITELIST_INIT_VER); }
public static String getMitmproxyOpts(SharedPreferences p) { return(p.getString(PREF_MITMPROXY_OPTS, "")); }
+ public static boolean isPortMappingEnabled(SharedPreferences p) { return(p.getBoolean(PREF_PORT_MAPPING_ENABLED, true)); }
public static String asString(Context ctx) {
SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(ctx);
diff --git a/app/src/main/jni/core/CMakeLists.txt b/app/src/main/jni/core/CMakeLists.txt
index 456412b3..ef559f50 100644
--- a/app/src/main/jni/core/CMakeLists.txt
+++ b/app/src/main/jni/core/CMakeLists.txt
@@ -10,6 +10,7 @@ add_library(capture SHARED
blacklist.c
pcap_utils.c
log_writer.c
+ port_map.c
jni_impl.c)
# nDPI
diff --git a/app/src/main/jni/core/capture_vpn.c b/app/src/main/jni/core/capture_vpn.c
index 632b8230..bbc32e2a 100644
--- a/app/src/main/jni/core/capture_vpn.c
+++ b/app/src/main/jni/core/capture_vpn.c
@@ -19,6 +19,7 @@
#include "pcapdroid.h"
#include "common/utils.h"
+#include "port_map.h"
/* ******************************************************* */
@@ -146,15 +147,9 @@ static bool check_dns_req_allowed(pcapdroid_t *pd, zdtun_conn_t *conn, pkt_conte
const zdtun_5tuple_t *tuple = pctx->tuple;
if(new_dns_server != 0) {
- // Reload DNS server
+ log_i("Using new DNS server");
pd->vpn.ipv4.dns_server = new_dns_server;
new_dns_server = 0;
-
- zdtun_ip_t ip = {0};
- ip.ip4 = pd->vpn.ipv4.dns_server;
- zdtun_set_dnat_info(pd->zdt, &ip, htons(53), 4);
-
- log_i("Using new DNS server");
}
if(pctx->tuple->ipproto == IPPROTO_ICMP)
@@ -201,7 +196,9 @@ static bool check_dns_req_allowed(pcapdroid_t *pd, zdtun_conn_t *conn, pkt_conte
* Direct the packet to the public DNS server. Checksum recalculation is not strictly necessary
* here as zdtun will pd the connection.
*/
- zdtun_conn_dnat(conn);
+ zdtun_ip_t ip = {0};
+ ip.ip4 = pd->vpn.ipv4.dns_server;
+ zdtun_conn_dnat(conn, &ip, htons(53), 4);
}
return(true);
@@ -465,12 +462,6 @@ int run_vpn(pcapdroid_t *pd) {
zdtun_set_socks5_userpass(zdt, pd->socks5.proxy_user, pd->socks5.proxy_pass);
}
- if(pd->vpn.ipv4.enabled) {
- zdtun_ip_t ip = {0};
- ip.ip4 = pd->vpn.ipv4.dns_server;
- zdtun_set_dnat_info(zdt, &ip, ntohs(53), 4);
- }
-
pd_refresh_time(pd);
next_purge_ms = pd->now_ms + PERIODIC_PURGE_TIMEOUT_MS;
@@ -551,9 +542,13 @@ int run_vpn(pcapdroid_t *pd) {
pd_conn_t *data = zdtun_conn_get_userdata(conn);
// To be run before pd_process_packet/process_payload
- if((data->sent_pkts == 0) && should_proxy(pd, tuple)) {
- zdtun_conn_proxy(conn);
- data->proxied = true;
+ if(data->sent_pkts == 0) {
+ if(pd_check_port_map(conn))
+ /* port mapping applied */;
+ else if(should_proxy(pd, tuple)) {
+ zdtun_conn_proxy(conn);
+ data->proxied = true;
+ }
}
pd_process_packet(pd, &pkt, true, tuple, data, get_pkt_timestamp(pd, &tv), &pctx);
@@ -640,6 +635,7 @@ int run_vpn(pcapdroid_t *pd) {
}
}
+ pd_reset_port_map();
zdtun_finalize(zdt);
#if ANDROID
diff --git a/app/src/main/jni/core/jni_impl.c b/app/src/main/jni/core/jni_impl.c
index ae930f54..59031848 100644
--- a/app/src/main/jni/core/jni_impl.c
+++ b/app/src/main/jni/core/jni_impl.c
@@ -24,6 +24,7 @@
#include "pcap_utils.h"
#include "common/utils.h"
#include "log_writer.h"
+#include "port_map.h"
// This files contains functions to make the capture core communicate
// with the Android system.
@@ -739,6 +740,28 @@ Java_com_emanuelef_remote_1capture_CaptureService_setPrivateDnsBlocked(JNIEnv *e
/* ******************************************************* */
+JNIEXPORT void JNICALL
+Java_com_emanuelef_remote_1capture_CaptureService_addPortMapping(JNIEnv *env, jclass clazz, jint ipproto,
+ jint orig_port, jint redirect_port, jstring redirect_ip) {
+ zdtun_ip_t ip;
+
+ const char *ip_s = (*env)->GetStringUTFChars(env, redirect_ip, 0);
+ int ipver = zdtun_parse_ip(ip_s, &ip);
+ (*env)->ReleaseStringUTFChars(env, redirect_ip, ip_s);
+
+ if(ipver < 0) {
+ log_e("addPortMapping invalid IP");
+ return;
+ }
+
+ if(!pd_add_port_map(ipver, ipproto, orig_port, redirect_port, &ip)) {
+ log_e("addPortMapping failed");
+ return;
+ }
+}
+
+/* ******************************************************* */
+
JNIEXPORT jboolean JNICALL
Java_com_emanuelef_remote_1capture_CaptureService_reloadBlocklist(JNIEnv *env, jclass clazz,
jobject ld) {
diff --git a/app/src/main/jni/core/port_map.c b/app/src/main/jni/core/port_map.c
new file mode 100644
index 00000000..bd9edbcc
--- /dev/null
+++ b/app/src/main/jni/core/port_map.c
@@ -0,0 +1,106 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2022 - Emanuele Faranda
+ */
+
+#include "common/memtrack.h"
+#include "log_writer.h"
+#include "port_map.h"
+
+typedef struct {
+ zdtun_ip_t redirect_ip;
+ int ipver;
+ int orig_port;
+ int redirect_port;
+} port_map_t;
+
+typedef struct {
+ port_map_t *items;
+ int num_items;
+} port_map_list_t;
+
+static struct {
+ port_map_list_t tcp;
+ port_map_list_t udp;
+} mappings;
+
+/* ******************************************************* */
+
+static inline port_map_list_t* get_map_list(int ipproto) {
+ if(ipproto == IPPROTO_TCP)
+ return &mappings.tcp;
+ else if(ipproto == IPPROTO_UDP)
+ return &mappings.udp;
+ else
+ return NULL;
+}
+
+/* ******************************************************* */
+
+bool pd_add_port_map(int ipver, int ipproto, int orig_port, int redirect_port, const zdtun_ip_t *redirect_ip) {
+ port_map_list_t *mlist = get_map_list(ipproto);
+ if(!mlist)
+ return false;
+
+ mlist->items = (port_map_t*) pd_realloc(mlist->items, (++mlist->num_items) * sizeof(port_map_t));
+ if(!mlist->items) {
+ mlist->num_items = 0;
+ return false;
+ }
+
+ port_map_t *mapping = &mlist->items[mlist->num_items - 1];
+ mapping->orig_port = htons(orig_port);
+ mapping->ipver = ipver;
+ mapping->redirect_ip = *redirect_ip;
+ mapping->redirect_port = htons(redirect_port);
+
+ return true;
+}
+
+/* ******************************************************* */
+
+bool pd_check_port_map(zdtun_conn_t *conn) {
+ const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn);
+ port_map_list_t *mlist = get_map_list(tuple->ipproto);
+ if(!mlist)
+ return false;
+
+ for(int i=0; inum_items; i++) {
+ port_map_t *mapping = &mlist->items[i];
+
+ if(mapping->orig_port == tuple->dst_port) {
+ log_d("Port mapping found: %d -> %d", ntohs(tuple->dst_port), ntohs(mapping->redirect_port));
+ zdtun_conn_dnat(conn, &mapping->redirect_ip, mapping->redirect_port, mapping->ipver);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* ******************************************************* */
+
+static void clear_map_list(port_map_list_t *mlist) {
+ pd_free(mlist->items);
+ mlist->items = NULL;
+ mlist->num_items = 0;
+}
+
+void pd_reset_port_map() {
+ clear_map_list(&mappings.tcp);
+ clear_map_list(&mappings.udp);
+}
\ No newline at end of file
diff --git a/app/src/main/jni/core/port_map.h b/app/src/main/jni/core/port_map.h
new file mode 100644
index 00000000..6c1b723e
--- /dev/null
+++ b/app/src/main/jni/core/port_map.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2022 - Emanuele Faranda
+ */
+
+#ifndef PCAPDROID_PORTMAP_H
+#define PCAPDROID_PORTMAP_H
+
+#include
+#include "zdtun.h"
+
+bool pd_add_port_map(int ipver, int ipproto, int orig_port, int redirect_port, const zdtun_ip_t *redirect_ip);
+bool pd_check_port_map(zdtun_conn_t *conn);
+void pd_reset_port_map();
+
+#endif //PCAPDROID_PORTMAP_H
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 00000000..3e691ff6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/add_port_mapping_dialog.xml b/app/src/main/res/layout/add_port_mapping_dialog.xml
new file mode 100644
index 00000000..7e1b27f2
--- /dev/null
+++ b/app/src/main/res/layout/add_port_mapping_dialog.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dropdown_item.xml b/app/src/main/res/layout/dropdown_item.xml
new file mode 100644
index 00000000..5e5488d5
--- /dev/null
+++ b/app/src/main/res/layout/dropdown_item.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/port_mapping_item.xml b/app/src/main/res/layout/port_mapping_item.xml
new file mode 100644
index 00000000..40c63e6b
--- /dev/null
+++ b/app/src/main/res/layout/port_mapping_item.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/port_mapping_menu.xml b/app/src/main/res/menu/port_mapping_menu.xml
new file mode 100644
index 00000000..60f4450c
--- /dev/null
+++ b/app/src/main/res/menu/port_mapping_menu.xml
@@ -0,0 +1,19 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63903955..ce4d2ea6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -424,4 +424,14 @@
Add to whitelist
Remove from whitelist
Do you really want to reset these stats?
+ Port mapping
+ Configure port mapping rules to redirect connections to a different host or port
+ Add
+ Redirect to:
+ Original port
+ required
+ Destination IP address
+ Destination port
+ This port mapping already exists
+ Delete the selected items?
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 2940b20a..a4e3ae53 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -113,6 +113,12 @@
app:iconSpaceReserved="false"
app:defaultValue="8050"
app:useSimpleSummaryProvider="true" />
+
+
@@ -134,8 +140,7 @@
android:key="vpn_exceptions"
app:title="@string/vpn_exemptions"
app:summary="@string/vpn_exemptions_summary"
- app:iconSpaceReserved="false"
- app:fragment="com.emanuelef.remote_capture.fragments.VpnExceptions" />
+ app:iconSpaceReserved="false" />