From 92d9b7bd7fa987da0302e6e22ebcb8307052a5d1 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Fri, 6 Feb 2026 15:05:08 +0100 Subject: [PATCH] Move SOCKS5 host resolution from CaptureHelper to CaptureService Host resolution was performed in CaptureHelper before starting the service, but this was skipped for always-on VPN (system starts the service directly) and BootReceiver (starts service without CaptureHelper). Resolve the SOCKS5 proxy hostname in the capture thread using the underlying (non-VPN) network saved before VPN establishment. This covers all startup paths and also re-enables hostname support for the intent-based API (disabled in 6ca1073 to work around a UI glitch that no longer applies since resolution is now async in the service). --- .../remote_capture/CaptureHelper.java | 74 ++----------------- .../remote_capture/CaptureService.java | 35 ++++++++- .../activities/CaptureCtrl.java | 2 +- .../activities/MainActivity.java | 2 +- .../remote_capture/model/CaptureSettings.java | 3 +- .../emanuelef/remote_capture/model/Prefs.java | 1 + docs/app_api.md | 5 +- 7 files changed, 46 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java index 4daae5e9..26570a71 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with PCAPdroid. If not, see . * - * Copyright 2020-21 - Emanuele Faranda + * Copyright 2020-26 - Emanuele Faranda */ package com.emanuelef.remote_capture; @@ -24,8 +24,6 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.VpnService; -import android.os.Handler; -import android.os.Looper; import com.emanuelef.remote_capture.interfaces.CaptureStartListener; import com.emanuelef.remote_capture.model.CaptureSettings; @@ -38,20 +36,15 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; -import java.net.InetAddress; -import java.net.UnknownHostException; - public class CaptureHelper { private static final String TAG = "CaptureHelper"; private final Context mContext; private final @Nullable ActivityResultLauncher mLauncher; - private final boolean mResolveHosts; private CaptureSettings mSettings; private CaptureStartListener mListener; - public CaptureHelper(ComponentActivity activity, boolean resolve_hosts) { + public CaptureHelper(ComponentActivity activity) { mContext = activity; - mResolveHosts = resolve_hosts; mLauncher = activity.registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), this::captureServiceResult); } @@ -59,13 +52,12 @@ public class CaptureHelper { /** Note: This constructor does not handle the first-time VPN prepare */ public CaptureHelper(Context context) { mContext = context; - mResolveHosts = true; mLauncher = null; } private void captureServiceResult(final ActivityResult result) { if(result.getResultCode() == Activity.RESULT_OK) - resolveHosts(); + startCaptureOk(); else if(mListener != null) { Utils.showToastLong(mContext, R.string.vpn_setup_failed); mListener.onCaptureStartResult(false); @@ -81,62 +73,6 @@ public class CaptureHelper { mListener.onCaptureStartResult(true); } - private static String resolveHost(String host) { - Log.d(TAG, "Resolving host: " + host); - - try { - return InetAddress.getByName(host).getHostAddress(); - } catch (UnknownHostException ignored) {} - - return null; - } - - private static String doResolveHosts(CaptureSettings settings) { - // NOTE: hosts must be resolved before starting the VPN and in a separate thread - String resolved; - - if(settings == null) - return null; - - if(settings.socks5_enabled) { - if ((resolved = resolveHost(settings.socks5_proxy_address)) == null) - return settings.socks5_proxy_address; - else if (!resolved.equals(settings.socks5_proxy_address)) { - Log.i(TAG, "Resolved SOCKS5 proxy address: " + resolved); - settings.socks5_proxy_address = resolved; - } - } - - return null; - } - - private void resolveHosts() { - if (!mResolveHosts) { - startCaptureOk(); - return; - } - - final Handler handler = new Handler(Looper.getMainLooper()); - - (new Thread(() -> { - String failed_host = doResolveHosts(mSettings); - - handler.post(() -> { - if(mSettings == null) { - mListener.onCaptureStartResult(false); - return; - } - - if(failed_host == null) - startCaptureOk(); - else { - Utils.showToastLong(mContext, R.string.host_resolution_failed, failed_host); - mListener.onCaptureStartResult(false); - } - }); - })).start(); - } - public void startCapture(CaptureSettings settings) { if(CaptureService.isServiceActive()) CaptureService.stopService(); @@ -144,7 +80,7 @@ public class CaptureHelper { mSettings = settings; if(settings.root_capture || settings.readFromPcap()) { - resolveHosts(); + startCaptureOk(); return; } @@ -177,7 +113,7 @@ public class CaptureHelper { else if (mListener != null) mListener.onCaptureStartResult(false); } else - resolveHosts(); + startCaptureOk(); } public void setListener(CaptureStartListener listener) { diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java index b0d444f2..db6a004b 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with PCAPdroid. If not, see . * - * Copyright 2020-24 - Emanuele Faranda + * Copyright 2020-26 - Emanuele Faranda */ package com.emanuelef.remote_capture; @@ -143,6 +143,7 @@ public class CaptureService extends VpnService implements Runnable { private NotificationCompat.Builder mStatusBuilder; private NotificationCompat.Builder mMalwareBuilder; private long mMonitoredNetwork; + private Network mUnderlyingNetwork; private ConnectivityManager.NetworkCallback mNetworkCallback; private AppsResolver mNativeAppsResolver; // can only be accessed by native code to avoid concurrency issues private Geolocation mNativeGeolocation; // only native @@ -338,6 +339,7 @@ public class CaptureService extends VpnService implements Runnable { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ConnectivityManager cm = (ConnectivityManager) getSystemService(Service.CONNECTIVITY_SERVICE); Network net = cm.getActiveNetwork(); + mUnderlyingNetwork = net; if(net != null) { handleLinkProperties(cm.getLinkProperties(net)); @@ -1156,10 +1158,39 @@ public class CaptureService extends VpnService implements Runnable { return((INSTANCE != null) ? INSTANCE.mSettings : null); } + // Resolve hostnames that the native code needs as IPs. + // Uses the underlying (non-VPN) network to avoid routing through the VPN tunnel. + private boolean resolveHosts() { + if(!mSocks5Enabled || mSettings.tls_decryption || mSocks5Address.isEmpty()) + return true; + + if((Build.VERSION.SDK_INT < Build.VERSION_CODES.M) || (mUnderlyingNetwork == null)) + return true; + + try { + InetAddress resolved = mUnderlyingNetwork.getByName(mSocks5Address); + String ip = resolved.getHostAddress(); + if(!ip.equals(mSocks5Address)) { + Log.i(TAG, "Resolved SOCKS5 proxy: " + mSocks5Address + " -> " + ip); + mSocks5Address = ip; + } + return true; + } catch (UnknownHostException e) { + Log.e(TAG, "Could not resolve SOCKS5 proxy: " + mSocks5Address); + mHandler.post(() -> Utils.showToastLong(this, R.string.host_resolution_failed, mSocks5Address)); + return false; + } + } + // Inside the mCaptureThread @Override public void run() { - if(mSettings.root_capture || mSettings.readFromPcap()) { + boolean hostResolved = resolveHosts(); + mUnderlyingNetwork = null; + + if(!hostResolved) { + // fall through to cleanup + } else if(mSettings.root_capture || mSettings.readFromPcap()) { // Check for INTERACT_ACROSS_USERS, required to query apps of other users/work profiles if(mSettings.root_capture && (checkCallingOrSelfPermission(Utils.INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED)) { boolean success = Utils.rootGrantPermission(this, Utils.INTERACT_ACROSS_USERS); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java index e765d905..8bdcdc1e 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java @@ -98,7 +98,7 @@ public class CaptureCtrl extends AppCompatActivity { getWindow().addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); super.onCreate(savedInstanceState); - mCapHelper = new CaptureHelper(this, false); + mCapHelper = new CaptureHelper(this); mCapHelper.setListener(success -> { setResult(success ? RESULT_OK : RESULT_CANCELED, null); finish(); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java index 4c73d6da..0f478b4e 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java @@ -180,7 +180,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig initAppState(); checkPermissions(); - mCapHelper = new CaptureHelper(this, true); + mCapHelper = new CaptureHelper(this); mCapHelper.setListener(success -> { if(!success) { Log.w(TAG, "Capture start failed"); diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java b/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java index a6bf6f7e..9903644f 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/CaptureSettings.java @@ -75,7 +75,8 @@ public class CaptureSettings implements Serializable { collector_port = getInt(intent, Prefs.PREF_COLLECTOR_PORT_KEY, 1234); http_server_port = getInt(intent, Prefs.PREF_HTTP_SERVER_PORT, 8080); socks5_enabled = getBool(intent, Prefs.PREF_SOCKS5_ENABLED_KEY, false); - socks5_proxy_address = getString(intent, Prefs.PREF_SOCKS5_PROXY_IP_KEY, "0.0.0.0"); + socks5_proxy_address = getString(intent, Prefs.PREF_SOCKS5_PROXY_HOST_KEY, + getString(intent, Prefs.PREF_SOCKS5_PROXY_IP_KEY, "0.0.0.0")); socks5_proxy_port = getInt(intent, Prefs.PREF_SOCKS5_PROXY_PORT_KEY, 8080); socks5_username = getString(intent, Prefs.PREF_SOCKS5_USERNAME_KEY, ""); socks5_password = getString(intent, Prefs.PREF_SOCKS5_PASSWORD_KEY, ""); 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 c2f15d88..8461c3fd 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 @@ -63,6 +63,7 @@ public class Prefs { public static final String PREF_COLLECTOR_IP_KEY = "collector_ip_address"; public static final String PREF_COLLECTOR_PORT_KEY = "collector_port"; public static final String PREF_SOCKS5_PROXY_IP_KEY = "socks5_proxy_ip_address"; + public static final String PREF_SOCKS5_PROXY_HOST_KEY = "socks5_proxy_host"; public static final String PREF_SOCKS5_PROXY_PORT_KEY = "socks5_proxy_port"; public static final String PREF_CAPTURE_INTERFACE = "capture_interface"; public static final String PREF_MALWARE_DETECTION = "malware_detection"; diff --git a/docs/app_api.md b/docs/app_api.md index 43d0f0c1..9229cdb4 100644 --- a/docs/app_api.md +++ b/docs/app_api.md @@ -105,7 +105,7 @@ As shown above, the capture settings can be specified by using intent extras. Th | http_server_port | int | | | the HTTP server port in http_server mode | | pcap_uri | string | | | the URI for the PCAP dump in pcap_file mode (overrides pcap_name) | | socks5_enabled | bool | | vpn | true to redirect the TCP connections to a SOCKS5 proxy | -| socks5_proxy_ip_address | string | | vpn | the SOCKS5 proxy IP address | +| socks5_proxy_ip_address | string | | vpn | (deprecated) the SOCKS5 proxy IP address. Alias for socks5_proxy_host since version 90 | | socks5_proxy_port | int | | vpn | the SOCKS5 proxy port | | root_capture | bool | | | true to capture packets in root mode, false to use the VPNService | | pcapdroid_trailer | bool | | | (deprecated) alias for dump_extensions | @@ -126,13 +126,14 @@ As shown above, the capture settings can be specified by using intent extras. Th | sslkeylog_name | bool | 89 | vpn | dump the SSLKEYLOGFILE to the /sdcard/Downloads/PCAPDroid directory with the given name | | decryption_rules | string | 89 | vpn | provide decryption rules as json (e.g. [{"type":"APP","value":"com.example.app"},{"type":"IP","value":"1.1.1.1"}]) | | full_payload | bool | 89 | | true to dump the full payload of the packets | +| socks5_proxy_host | string | 90 | vpn | the SOCKS5 proxy IP address or hostname | \*: paid feature The `Ver` column indicates the minimum PCAPdroid version required to use the given parameter. The PCAPdroid version can be queried via the `get_status` action as explained below. The `Mode` column indicates if the option applies to any mode or only to the VPN or root mode. -*NOTE*: for security reasons, since version 1.5.3 you cannot specify a remote server IP address in `collector_ip_address` or in `socks5_proxy_ip_address`. If you really want to do this, you should first set such a remote IP address via the PCAPdroid gui and only then invoke the API. +*NOTE*: for security reasons, since version 1.5.3 you cannot specify a remote server address in `collector_ip_address` or in `socks5_proxy_ip_address`/`socks5_proxy_host`. If you really want to do this, you should first set such a remote address via the PCAPdroid gui and only then invoke the API. *NOTE*: since version 1.6.0, the `pcap_uri` behavior is changed as described in the `Dumping PCAP to file` section below