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