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 79829f5e..aaa1b2e7 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java @@ -23,6 +23,8 @@ import android.content.ActivityNotFoundException; 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; @@ -34,7 +36,11 @@ 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 ComponentActivity mActivity; private final ActivityResultLauncher mLauncher; private CaptureSettings mSettings; @@ -48,7 +54,7 @@ public CaptureHelper(ComponentActivity activity) { private void captureServiceResult(final ActivityResult result) { if(result.getResultCode() == Activity.RESULT_OK) - startCaptureOk(); + resolveHosts(); else if(mListener != null) { Utils.showToastLong(mActivity, R.string.vpn_setup_failed); mListener.onCaptureStartResult(false); @@ -64,6 +70,47 @@ private void startCaptureOk() { mListener.onCaptureStartResult(true); } + private String resolveHost(String host) { + Log.d(TAG, "Resolving host: " + host); + + try { + return InetAddress.getByName(host).getHostAddress(); + } catch (UnknownHostException ignored) {} + + return null; + } + + private String doResolveHosts() { + // NOTE: hosts must be resolved before starting the VPN and in a separate thread + String resolved; + + if((resolved = resolveHost(mSettings.socks5_proxy_address)) == null) + return mSettings.socks5_proxy_address; + else if(!resolved.equals(mSettings.socks5_proxy_address)) { + Log.i(TAG, "Resolved SOCKS5 proxy address: " + resolved); + mSettings.socks5_proxy_address = resolved; + } + + return null; + } + + private void resolveHosts() { + final Handler handler = new Handler(Looper.getMainLooper()); + + (new Thread(() -> { + String failed_host = doResolveHosts(); + + handler.post(() -> { + if(failed_host == null) + startCaptureOk(); + else { + Utils.showToastLong(mActivity, R.string.host_resolution_failed, failed_host); + mListener.onCaptureStartResult(false); + } + }); + })).start(); + } + public void startCapture(CaptureSettings settings) { if(CaptureService.isServiceActive()) CaptureService.stopService(); @@ -71,7 +118,7 @@ public void startCapture(CaptureSettings settings) { mSettings = settings; if(settings.root_capture || settings.readFromPcap()) { - startCaptureOk(); + resolveHosts(); return; } @@ -93,7 +140,7 @@ public void startCapture(CaptureSettings settings) { }) .show(); } else - startCaptureOk(); + resolveHosts(); } public void setListener(CaptureStartListener listener) { 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 dc9ce214..f9fa7805 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java @@ -484,6 +484,10 @@ public static boolean isLocalNetworkAddress(InetAddress checkAddress) { } public static boolean isLocalNetworkAddress(String checkAddress) { + // this check is necessary as otherwise host resolution would be triggered on the main thread + if(!validateIpAddress(checkAddress)) + return false; + try { return isLocalNetworkAddress(InetAddress.getByName(checkAddress)); } catch (UnknownHostException ignored) { 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 531756ba..e87fba47 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 @@ -220,7 +220,7 @@ private String checkRemoteServerNotAllowed(CaptureSettings settings) { if(settings.socks5_enabled && !Utils.isLocalNetworkAddress(settings.socks5_proxy_address) && - !Prefs.getSocks5ProxyAddress(prefs).equals(settings.socks5_proxy_address)) + !Prefs.getSocks5ProxyHost(prefs).equals(settings.socks5_proxy_address)) return settings.socks5_proxy_address; // ok 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 5927d06b..e6ea0f7e 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 @@ -158,7 +158,7 @@ protected void onCreate(Bundle savedInstanceState) { mCapHelper = new CaptureHelper(this); mCapHelper.setListener(success -> { if(!success) { - Log.w(TAG, "VPN request failed"); + Log.w(TAG, "Capture start failed"); appStateReady(); } }); @@ -684,7 +684,7 @@ private boolean showRemoteServerAlert() { return false; // already acknowledged if(((Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.UDP_EXPORTER) && !Utils.isLocalNetworkAddress(Prefs.getCollectorIp(mPrefs))) || - (Prefs.getSocks5Enabled(mPrefs) && !Utils.isLocalNetworkAddress(Prefs.getSocks5ProxyAddress(mPrefs)))) { + (Prefs.getSocks5Enabled(mPrefs) && !Utils.isLocalNetworkAddress(Prefs.getSocks5ProxyHost(mPrefs)))) { Log.i(TAG, "Showing possible scan notice"); AlertDialog dialog = new AlertDialog.Builder(this) diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/prefs/Socks5Settings.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/prefs/Socks5Settings.java index 1bbb1971..a7c8ed4d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/prefs/Socks5Settings.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/prefs/Socks5Settings.java @@ -33,7 +33,7 @@ import java.util.Objects; public class Socks5Settings extends PreferenceFragmentCompat { - private EditTextPreference mProxyIp; + private EditTextPreference mProxyHost; private EditTextPreference mProxyPort; private EditTextPreference mUsername; private EditTextPreference mPassword; @@ -44,8 +44,8 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S setPreferencesFromResource(R.xml.socks5_preferences, rootKey); /* SOCKS5 Proxy IP validation */ - mProxyIp = Objects.requireNonNull(findPreference(Prefs.PREF_SOCKS5_PROXY_IP_KEY)); - mProxyIp.setOnPreferenceChangeListener((preference, newValue) -> Utils.validateIpAddress(newValue.toString())); + mProxyHost = Objects.requireNonNull(findPreference(Prefs.PREF_SOCKS5_PROXY_IP_KEY)); + mProxyHost.setOnPreferenceChangeListener((preference, newValue) -> Utils.validateHost(newValue.toString())); /* SOCKS5 Proxy port validation */ mProxyPort = Objects.requireNonNull(findPreference(Prefs.PREF_SOCKS5_PROXY_PORT_KEY)); @@ -70,7 +70,7 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S } private void toggleVisisiblity(boolean socks5_enabled, boolean auth_enabled) { - mProxyIp.setVisible(socks5_enabled); + mProxyHost.setVisible(socks5_enabled); mProxyPort.setVisible(socks5_enabled); mSocks5AuthEnabled.setVisible(socks5_enabled); 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 cb8fb7f8..5c752308 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 @@ -44,7 +44,7 @@ public CaptureSettings(Context ctx, SharedPreferences prefs) { collector_port = Prefs.getCollectorPort(prefs); http_server_port = Prefs.getHttpServerPort(prefs); socks5_enabled = Prefs.getSocks5Enabled(prefs); - socks5_proxy_address = Prefs.getSocks5ProxyAddress(prefs); + socks5_proxy_address = Prefs.getSocks5ProxyHost(prefs); socks5_proxy_port = Prefs.getSocks5ProxyPort(prefs); socks5_username = Prefs.isSocks5AuthEnabled(prefs) ? Prefs.getSocks5Username(prefs) : ""; socks5_password = Prefs.isSocks5AuthEnabled(prefs) ? Prefs.getSocks5Password(prefs) : ""; 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 438dbccc..d097f278 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 @@ -170,7 +170,7 @@ public static void setPortMappingEnabled(SharedPreferences p, boolean enabled) { public static int getHttpServerPort(SharedPreferences p) { return(Integer.parseInt(p.getString(Prefs.PREF_HTTP_SERVER_PORT, "8080"))); } public static boolean getTlsDecryptionEnabled(SharedPreferences p) { return(p.getBoolean(PREF_TLS_DECRYPTION_KEY, false)); } public static boolean getSocks5Enabled(SharedPreferences p) { return(p.getBoolean(PREF_SOCKS5_ENABLED_KEY, false)); } - public static String getSocks5ProxyAddress(SharedPreferences p) { return(p.getString(PREF_SOCKS5_PROXY_IP_KEY, "0.0.0.0")); } + public static String getSocks5ProxyHost(SharedPreferences p) { return(p.getString(PREF_SOCKS5_PROXY_IP_KEY, "0.0.0.0")); } public static int getSocks5ProxyPort(SharedPreferences p) { return(Integer.parseInt(p.getString(Prefs.PREF_SOCKS5_PROXY_PORT_KEY, "8080"))); } public static boolean isSocks5AuthEnabled(SharedPreferences p) { return(p.getBoolean(PREF_SOCKS5_AUTH_ENABLED_KEY, false)); } public static String getSocks5Username(SharedPreferences p) { return(p.getString(PREF_SOCKS5_USERNAME_KEY, "")); } diff --git a/app/src/main/jni/core/capture_vpn.c b/app/src/main/jni/core/capture_vpn.c index ac193ed6..cfed4117 100644 --- a/app/src/main/jni/core/capture_vpn.c +++ b/app/src/main/jni/core/capture_vpn.c @@ -473,9 +473,7 @@ int run_vpn(pcapdroid_t *pd) { new_dns_server = 0; if(pd->socks5.enabled) { - zdtun_ip_t dnatip = {0}; - dnatip.ip4 = pd->socks5.proxy_ip; - zdtun_set_socks5_proxy(zdt, &dnatip, pd->socks5.proxy_port, 4); + zdtun_set_socks5_proxy(zdt, &pd->socks5.proxy_ip, pd->socks5.proxy_port, pd->socks5.proxy_ipver); if(pd->socks5.proxy_user[0] && pd->socks5.proxy_pass[0]) zdtun_set_socks5_userpass(zdt, pd->socks5.proxy_user, pd->socks5.proxy_pass); diff --git a/app/src/main/jni/core/jni_impl.c b/app/src/main/jni/core/jni_impl.c index 20cbde7c..b011aa16 100644 --- a/app/src/main/jni/core/jni_impl.c +++ b/app/src/main/jni/core/jni_impl.c @@ -599,7 +599,7 @@ Java_com_emanuelef_remote_1capture_CaptureService_runPacketLoop(JNIEnv *env, jcl }, .socks5 = { .enabled = (bool) getIntPref(env, vpn, "getSocks5Enabled"), - .proxy_ip = getIPv4Pref(env, vpn, "getSocks5ProxyAddress"), + .proxy_ip = getIPPref(env, vpn, "getSocks5ProxyAddress", &pd.socks5.proxy_ipver), .proxy_port = htons(getIntPref(env, vpn, "getSocks5ProxyPort")), }, .malware_detection = { @@ -1149,6 +1149,32 @@ u_int32_t getIPv4Pref(JNIEnv *env, jobject vpn_inst, const char *key) { /* ******************************************************* */ +zdtun_ip_t getIPPref(JNIEnv *env, jobject vpn_inst, const char *key, int *ip_ver) { + zdtun_ip_t rv = {}; + + jmethodID midMethod = jniGetMethodID(env, cls.vpn_service, key, "()Ljava/lang/String;"); + jstring obj = (*env)->CallObjectMethod(env, vpn_inst, midMethod); + + if(!jniCheckException(env)) { + const char *value = (*env)->GetStringUTFChars(env, obj, 0); + log_d("getIPPref(%s) = %s", key, value); + + if(*value) { + *ip_ver = zdtun_parse_ip(value, &rv); + + if(*ip_ver < 0) + log_e("%s() returned invalid IP address: %s", key, value); + } + + (*env)->ReleaseStringUTFChars(env, obj, value); + } + + (*env)->DeleteLocalRef(env, obj); + return(rv); +} + +/* ******************************************************* */ + struct in6_addr getIPv6Pref(JNIEnv *env, jobject vpn_inst, const char *key) { struct in6_addr addr = {0}; diff --git a/app/src/main/jni/core/pcapdroid.h b/app/src/main/jni/core/pcapdroid.h index b542d16d..3f904de6 100644 --- a/app/src/main/jni/core/pcapdroid.h +++ b/app/src/main/jni/core/pcapdroid.h @@ -239,8 +239,9 @@ typedef struct pcapdroid { struct { bool enabled; - u_int32_t proxy_ip; + zdtun_ip_t proxy_ip; u_int32_t proxy_port; + int proxy_ipver; char proxy_user[32]; char proxy_pass[32]; } socks5; @@ -395,6 +396,7 @@ uint16_t pd_ndpi2proto(ndpi_protocol proto); char* getStringPref(pcapdroid_t *pd, const char *key, char *buf, int bufsize); int getIntPref(JNIEnv *env, jobject vpn_inst, const char *key); +zdtun_ip_t getIPPref(JNIEnv *env, jobject vpn_inst, const char *key, int *ip_ver); uint32_t getIPv4Pref(JNIEnv *env, jobject vpn_inst, const char *key); struct in6_addr getIPv6Pref(JNIEnv *env, jobject vpn_inst, const char *key); void getApplicationByUid(pcapdroid_t *pd, jint uid, char *buf, int bufsize); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b422ec2c..66167556 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,6 +130,7 @@ SOCKS5 authentication Authenticate to the proxy via username and password Proxy IP address + Proxy host Proxy port Capture as root Allows PCAPdroid to run with other VPN apps @@ -486,4 +487,5 @@ Capture start failure. Ensure that the device is rooted with Magisk PCAP read error PCAP file loading aborted + "Could not resolve host %1$s diff --git a/app/src/main/res/xml/socks5_preferences.xml b/app/src/main/res/xml/socks5_preferences.xml index 4024eea4..4f1fd215 100644 --- a/app/src/main/res/xml/socks5_preferences.xml +++ b/app/src/main/res/xml/socks5_preferences.xml @@ -11,7 +11,7 @@ diff --git a/docs/app_api.md b/docs/app_api.md index 1d522a75..cb6a4503 100644 --- a/docs/app_api.md +++ b/docs/app_api.md @@ -85,8 +85,8 @@ 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 IP address of the SOCKS5 proxy | -| socks5_proxy_port | int | | vpn | the TCP port of the SOCKS5 proxy | +| socks5_proxy_ip_address | string | | vpn | the SOCKS5 proxy host | +| 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 | | | true to enable the PCAPdroid trailer | | capture_interface | string | | root | @inet \| any \| ifname - network interface to use in root mode |