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).
This commit is contained in:
emanuele-f
2026-02-06 15:05:08 +01:00
parent be671a20fa
commit 92d9b7bd7f
7 changed files with 46 additions and 76 deletions
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2020-21 - Emanuele Faranda
* Copyright 2020-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<Intent> 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) {
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2020-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);
@@ -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();
@@ -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");
@@ -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, "");
@@ -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";
+3 -2
View File
@@ -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