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" />