From c2df8436ab7312468b93cd10a155408ecfd964dd Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Sat, 1 May 2021 12:02:48 +0200 Subject: [PATCH] Implement ability to capture packets as root This requires a rooted device. It allows PCAPdroid to run with other VPN apps. --- .gitmodules | 5 +- DONATORS.txt => DONORS.txt | 0 app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 7 +- .../remote_capture/CaptureService.java | 130 ++- .../com/emanuelef/remote_capture/Utils.java | 25 + .../activities/AboutActivity.java | 5 + .../activities/MainActivity.java | 12 +- .../activities/SettingsActivity.java | 22 + .../activities/StatsActivity.java | 8 + .../emanuelef/remote_capture/model/Prefs.java | 2 + app/src/main/jni/CMakeLists.txt | 16 + app/src/main/jni/common/CMakeLists.txt | 3 + app/src/main/jni/common/uid_lru.c | 122 +++ .../jni_helpers.h => common/uid_lru.h} | 22 +- .../{vpnproxy-jni => common}/uid_resolver.c | 26 +- .../{vpnproxy-jni => common}/uid_resolver.h | 3 + .../jni_helpers.c => common/utils.c} | 98 +- app/src/main/jni/common/utils.h | 45 + app/src/main/jni/pcapd/CMakeLists.txt | 36 + app/src/main/jni/pcapd/nl_utils.c | 148 +++ app/src/main/jni/pcapd/nl_utils.h | 41 + app/src/main/jni/pcapd/pcapd.c | 576 +++++++++++ app/src/main/jni/pcapd/pcapd.h | 38 + app/src/main/jni/vpnproxy-jni/CMakeLists.txt | 21 +- app/src/main/jni/vpnproxy-jni/capture_proxy.c | 438 +++++++++ app/src/main/jni/vpnproxy-jni/capture_root.c | 377 ++++++++ .../jni/vpnproxy-jni/{pcap.c => pcap_utils.c} | 2 +- .../jni/vpnproxy-jni/{pcap.h => pcap_utils.h} | 0 app/src/main/jni/vpnproxy-jni/utils.c | 59 -- app/src/main/jni/vpnproxy-jni/vpnproxy.c | 911 ++++++------------ app/src/main/jni/vpnproxy-jni/vpnproxy.h | 86 +- app/src/main/res/drawable/ic_github.xml | 8 - app/src/main/res/drawable/ic_money.xml | 5 + app/src/main/res/layout/about_activity.xml | 9 + app/src/main/res/layout/activity_stats.xml | 12 +- app/src/main/res/layout/connection_item.xml | 5 + app/src/main/res/menu/nav_items.xml | 6 +- app/src/main/res/values-it/strings.xml | 8 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/root_preferences.xml | 25 +- submodules/libpcap | 1 + submodules/zdtun | 2 +- 43 files changed, 2522 insertions(+), 848 deletions(-) rename DONATORS.txt => DONORS.txt (100%) create mode 100644 app/src/main/jni/CMakeLists.txt create mode 100644 app/src/main/jni/common/CMakeLists.txt create mode 100644 app/src/main/jni/common/uid_lru.c rename app/src/main/jni/{vpnproxy-jni/jni_helpers.h => common/uid_lru.h} (61%) rename app/src/main/jni/{vpnproxy-jni => common}/uid_resolver.c (92%) rename app/src/main/jni/{vpnproxy-jni => common}/uid_resolver.h (93%) rename app/src/main/jni/{vpnproxy-jni/jni_helpers.c => common/utils.c} (63%) create mode 100644 app/src/main/jni/common/utils.h create mode 100644 app/src/main/jni/pcapd/CMakeLists.txt create mode 100644 app/src/main/jni/pcapd/nl_utils.c create mode 100644 app/src/main/jni/pcapd/nl_utils.h create mode 100644 app/src/main/jni/pcapd/pcapd.c create mode 100644 app/src/main/jni/pcapd/pcapd.h create mode 100644 app/src/main/jni/vpnproxy-jni/capture_proxy.c create mode 100644 app/src/main/jni/vpnproxy-jni/capture_root.c rename app/src/main/jni/vpnproxy-jni/{pcap.c => pcap_utils.c} (99%) rename app/src/main/jni/vpnproxy-jni/{pcap.h => pcap_utils.h} (100%) delete mode 100644 app/src/main/jni/vpnproxy-jni/utils.c delete mode 100644 app/src/main/res/drawable/ic_github.xml create mode 100644 app/src/main/res/drawable/ic_money.xml create mode 160000 submodules/libpcap diff --git a/.gitmodules b/.gitmodules index 2b6b2e03..0d331ea9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ [submodule "submodules/nDPI"] path = submodules/nDPI - url = https://github.com/ntop/nDPI + url = https://github.com/ntop/nDPI [submodule "submodules/zdtun"] path = submodules/zdtun url = https://github.com/emanuele-f/zdtun +[submodule "submodules/libpcap"] + path = submodules/libpcap + url = https://github.com/the-tcpdump-group/libpcap diff --git a/DONATORS.txt b/DONORS.txt similarity index 100% rename from DONATORS.txt rename to DONORS.txt diff --git a/app/build.gradle b/app/build.gradle index 6950fc52..c35bd6cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { externalNativeBuild { cmake { - path file('src/main/jni/vpnproxy-jni/CMakeLists.txt') + path file('src/main/jni/CMakeLists.txt') } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3829ccbf..e4150f1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,13 +16,16 @@ + android:extractNativeLibs="true" + android:theme="@style/AppTheme" + android:allowBackup="true" + android:fullBackupContent="true" + tools:targetApi="m"> 0) && (fd < fd_setsize)) { Log.d(TAG, "VPN fd: " + fd + " - FD_SETSIZE: " + fd_setsize); runPacketLoop(fd, this, Build.VERSION.SDK_INT); - } else + } else { Log.e(TAG, "Invalid VPN fd: " + fd); + stopThread(); + } } } @@ -546,6 +578,10 @@ public class CaptureService extends VpnService implements Runnable { public int getIPv6Enabled() { return(ipv6_enabled ? 1 : 0); } + public int isRootCapture() { return(root_capture ? 1 : 0); } + + public int getAppFilterUid() { return(app_filter_uid); } + // returns 1 if dumpPcapData should be called public int dumpPcapToJava() { return(((dump_mode == Prefs.DumpMode.HTTP_SERVER) || (dump_mode == Prefs.DumpMode.PCAP_FILE)) ? 1 : 0); @@ -635,6 +671,16 @@ public class CaptureService extends VpnService implements Runnable { }); } + public String getPcapdWorkingDir() { + return getCacheDir().getAbsolutePath(); + } + + public String getLibprogPath(String prog_name) { + // executable binaries are stored into the /lib folder of the app + String dir = getApplicationInfo().nativeLibraryDir; + return(dir + "/lib" + prog_name + ".so"); + } + public static native void runPacketLoop(int fd, CaptureService vpn, int sdk); public static native void stopPacketLoop(); public static native void askStatsDump(); 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 1a14af56..f97ea49d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java @@ -64,6 +64,7 @@ import com.emanuelef.remote_capture.model.AppDescriptor; import com.emanuelef.remote_capture.model.Prefs; import com.emanuelef.remote_capture.views.AppsListView; +import java.io.File; import java.math.BigInteger; import java.net.Inet4Address; import java.net.InetAddress; @@ -82,6 +83,7 @@ public class Utils { public static final String PCAP_HEADER = "d4c3b2a1020004000000000000000000ffff000065000000"; public static final int UID_UNKNOWN = -1; public static final int UID_NO_FILTER = -2; + private static Boolean root_available = null; public static String formatBytes(long bytes) { long divisor; @@ -466,4 +468,27 @@ public class Utils { return fname; } + + public static boolean isRootAvailable() { + if(root_available == null) { + String path = System.getenv("PATH"); + root_available = false; + + if(path != null) { + Log.d("isRootAvailable", "PATH = " + path); + + for(String part : path.split(":")) { + File f = new File(part + "/su"); + + if(f.exists()) { + Log.d("isRootAvailable", "'su' binary found at " + f.getAbsolutePath()); + root_available = true; + break; + } + } + } + } + + return root_available; + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/AboutActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/AboutActivity.java index dc53b5a4..55fddb4d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/AboutActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/AboutActivity.java @@ -43,5 +43,10 @@ public class AboutActivity extends BaseActivity { String localized = gplLicense.getText().toString(); gplLicense.setText(Html.fromHtml("" + localized + "")); gplLicense.setMovementMethod(LinkMovementMethod.getInstance()); + + TextView sourceLink = findViewById(R.id.app_source_link); + localized = sourceLink.getText().toString(); + sourceLink.setText(Html.fromHtml("" + localized + "")); + sourceLink.setMovementMethod(LinkMovementMethod.getInstance()); } } 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 f85934f4..61120852 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 @@ -98,6 +98,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig public static final String TELEGRAM_GROUP_NAME = "PCAPdroid"; public static final String GITHUB_PROJECT_URL = "https://github.com/emanuele-f/PCAPdroid"; public static final String GITHUB_DOCS_URL = "https://emanuele-f.github.io/PCAPdroid"; + public static final String DONATE_URL = "https://emanuele-f.github.io/PCAPdroid/donate"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -344,8 +345,8 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig startActivity(intent); } else Utils.showToast(this, R.string.capture_not_started); - } else if (id == R.id.action_open_github) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_PROJECT_URL)); + } else if (id == R.id.action_donate) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATE_URL)); startActivity(browserIntent); } else if (id == R.id.action_open_telegram) { openTelegram(); @@ -501,6 +502,11 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig private void startCaptureService() { appStateStarting(); + if(Prefs.isRootCaptureEnabled(mPrefs)) { + onActivityResult(REQUEST_CODE_VPN, RESULT_OK, null); + return; + } + Intent vpnPrepareIntent = VpnService.prepare(MainActivity.this); if (vpnPrepareIntent != null) @@ -519,7 +525,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig return; } - if(Utils.hasVPNRunning(this)) { + if(!Prefs.isRootCaptureEnabled(mPrefs) && Utils.hasVPNRunning(this)) { new AlertDialog.Builder(this) .setMessage(R.string.existing_vpn_confirm) .setPositiveButton(R.string.yes, (dialog, whichButton) -> startCaptureService()) 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 a04b4a23..0258221c 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 @@ -83,9 +83,12 @@ public class SettingsActivity extends BaseActivity { public static class SettingsFragment extends PreferenceFragmentCompat { private SwitchPreference mTlsDecryptionEnabled; // TODO rename + private SwitchPreference mRootCaptureEnabled; private EditTextPreference mSocks5ProxyIp; private EditTextPreference mSocks5ProxyPort; private Preference mTlsHelp; + private Preference mProxyPrefs; + private Preference mIpv6Enabled; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -97,6 +100,7 @@ public class SettingsActivity extends BaseActivity { setupOtherPrefs(); socks5ProxyHideShow(mTlsDecryptionEnabled.isChecked()); + rootCaptureHideShow(Utils.isRootAvailable() && mRootCaptureEnabled.isChecked()); } private boolean validatePort(String value) { @@ -129,6 +133,7 @@ public class SettingsActivity extends BaseActivity { } private void setupSocks5ProxyPrefs() { + mProxyPrefs = findPreference("proxy_prefs"); mTlsHelp = findPreference("tls_how_to"); mTlsDecryptionEnabled = findPreference(Prefs.PREF_TLS_DECRYPTION_ENABLED_KEY); @@ -187,6 +192,23 @@ public class SettingsActivity extends BaseActivity { return true; }); + + mRootCaptureEnabled = findPreference(Prefs.PREF_ROOT_CAPTURE); + + if(Utils.isRootAvailable()) { + mRootCaptureEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + rootCaptureHideShow((Boolean) newValue); + return true; + }); + } else + mRootCaptureEnabled.setVisible(false); + + mIpv6Enabled = findPreference(Prefs.PREF_IPV6_ENABLED); + } + + private void rootCaptureHideShow(boolean enabled) { + mProxyPrefs.setVisible(!enabled); + mIpv6Enabled.setVisible(!enabled); } } } \ No newline at end of file diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/StatsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/StatsActivity.java index fb99305d..dba500f2 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/StatsActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/StatsActivity.java @@ -34,6 +34,7 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; import android.widget.TableLayout; import android.widget.TextView; @@ -76,6 +77,13 @@ public class StatsActivity extends BaseActivity { mDnsQueries = findViewById(R.id.dns_queries); mDnsServer = findViewById(R.id.dns_server); + if(CaptureService.isCapturingAsRoot()) { + findViewById(R.id.dns_server_row).setVisibility(View.GONE); + findViewById(R.id.dns_queries_row).setVisibility(View.GONE); + findViewById(R.id.open_sockets_row).setVisibility(View.GONE); + findViewById(R.id.max_fd_row).setVisibility(View.GONE); + } + mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { 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 109eb1c7..1d52fe05 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 @@ -40,6 +40,7 @@ public class Prefs { public static final String PREF_IPV6_ENABLED = "ipv6_enabled"; public static final String PREF_APP_LANGUAGE = "app_language"; public static final String PREF_APP_THEME = "app_theme"; + public static final String PREF_ROOT_CAPTURE = "root_capture"; public enum DumpMode { NONE, @@ -70,4 +71,5 @@ public class Prefs { public static String getAppFilter(SharedPreferences p) { return(p.getString(PREF_APP_FILTER, "")); } public static boolean getIPv6Enabled(SharedPreferences p) { return(p.getBoolean(PREF_IPV6_ENABLED, false)); } public static boolean useEnglishLanguage(SharedPreferences p){ return("english".equals(p.getString(PREF_APP_LANGUAGE, "system")));} + public static boolean isRootCaptureEnabled(SharedPreferences p) { return(Utils.isRootAvailable() && p.getBoolean(PREF_ROOT_CAPTURE, false)); } } diff --git a/app/src/main/jni/CMakeLists.txt b/app/src/main/jni/CMakeLists.txt new file mode 100644 index 00000000..dc93dabe --- /dev/null +++ b/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.10.2) + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(ROOTDIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../..) + +# zdtun +set(ZDTUN_ROOT ${ROOTDIR}/submodules/zdtun) +include_directories(${ZDTUN_ROOT}) +add_subdirectory(${ZDTUN_ROOT} zdtun_build) + +# base +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +add_subdirectory(common) +add_subdirectory(pcapd) +add_subdirectory(vpnproxy-jni) diff --git a/app/src/main/jni/common/CMakeLists.txt b/app/src/main/jni/common/CMakeLists.txt new file mode 100644 index 00000000..fa29173c --- /dev/null +++ b/app/src/main/jni/common/CMakeLists.txt @@ -0,0 +1,3 @@ +project(common C) + +ADD_LIBRARY(common STATIC uid_lru.c utils.c uid_resolver.c) diff --git a/app/src/main/jni/common/uid_lru.c b/app/src/main/jni/common/uid_lru.c new file mode 100644 index 00000000..ae4808d6 --- /dev/null +++ b/app/src/main/jni/common/uid_lru.c @@ -0,0 +1,122 @@ +/* + * 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-21 - Emanuele Faranda + */ + +// A simple LRU implementation based on uthash +// Inspired by https://jehiah.cz/a/uthash + +#include +#include "uid_lru.h" +#include "third_party/uthash.h" + +struct cache_entry { + zdtun_5tuple_t key; + int uid; + UT_hash_handle hh; +}; + +struct uid_lru { + int max_size; + struct cache_entry *cache; +}; + +/* ******************************************************* */ + +uid_lru_t* uid_lru_init(int max_size) { + uid_lru_t *lru = (uid_lru_t*) malloc(sizeof(uid_lru_t)); + + if(!lru) + return NULL; + + lru->max_size = max_size; + lru->cache = NULL; + + return lru; +} + +/* ******************************************************* */ + +void uid_lru_destroy(uid_lru_t *lru) { + struct cache_entry *entry, *tmp; + + HASH_ITER(hh, lru->cache, entry, tmp) { + HASH_DELETE(hh, lru->cache, entry); + free(entry); + } + + free(lru); +} + +/* ******************************************************* */ + +static struct cache_entry* uid_lru_find_entry(uid_lru_t *lru, const zdtun_5tuple_t *tuple) { + struct cache_entry *entry; + + HASH_FIND(hh, lru->cache, tuple, sizeof(zdtun_5tuple_t), entry); + + if(entry) { + // Bring the entry to the front of the list + HASH_DELETE(hh, lru->cache, entry); + HASH_ADD(hh, lru->cache, key, sizeof(zdtun_5tuple_t), entry); + + return(entry); + } + + return NULL; +} + +/* ******************************************************* */ + +void uid_lru_add(uid_lru_t *lru, const zdtun_5tuple_t *tuple, int uid) { + struct cache_entry *entry, *tmp; + + entry = malloc(sizeof(struct cache_entry)); + + if(!entry) + return; + + entry->key = *tuple; + entry->uid = uid; + + HASH_ADD(hh, lru->cache, key, sizeof(zdtun_5tuple_t), entry); + + if(HASH_COUNT(lru->cache) > lru->max_size) { + // uthash guarantees that iteration order is same as insertion order + // see https://troydhanson.github.io/uthash/userguide.html#_sorting + HASH_ITER(hh, lru->cache, entry, tmp) { + // delete the oldest entry + HASH_DELETE(hh, lru->cache, entry); + free(entry); + break; + } + } +} + +/* ******************************************************* */ + +int uid_lru_find(uid_lru_t *lru, const zdtun_5tuple_t *tuple) { + struct cache_entry *entry = uid_lru_find_entry(lru, tuple); + + return(entry ? entry->uid : -2); +} + +/* ******************************************************* */ + +int uid_lru_size(uid_lru_t *lru) { + return HASH_COUNT(lru->cache); +} \ No newline at end of file diff --git a/app/src/main/jni/vpnproxy-jni/jni_helpers.h b/app/src/main/jni/common/uid_lru.h similarity index 61% rename from app/src/main/jni/vpnproxy-jni/jni_helpers.h rename to app/src/main/jni/common/uid_lru.h index c9e10ab0..31fc9115 100644 --- a/app/src/main/jni/vpnproxy-jni/jni_helpers.h +++ b/app/src/main/jni/common/uid_lru.h @@ -17,19 +17,17 @@ * Copyright 2020-21 - Emanuele Faranda */ -#ifndef __JNI_HELPERS_H__ -#define __JNI_HELPERS_H__ +#ifndef __IP_LRU_H__ +#define __IP_LRU_H__ -#include -#include -#include +#include "zdtun.h" -void init_log(int lvl, JNIEnv *env, jclass _vpnclass, jclass _vpn_inst); -void finish_log(); -void log_android(int prio, const char *fmt, ...); +typedef struct uid_lru uid_lru_t; -jclass jniFindClass(JNIEnv *env, const char *name); -jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature); -int jniCheckException(JNIEnv *env); +uid_lru_t* uid_lru_init(int max_size); +void uid_lru_destroy(uid_lru_t *lru); +void uid_lru_add(uid_lru_t *lru, const zdtun_5tuple_t *tuple, int uid); +int uid_lru_find(uid_lru_t *lru, const zdtun_5tuple_t *tuple); +int uid_lru_size(uid_lru_t *lru); -#endif // __JNI_HELPERS_H__ +#endif // __IP_LRU_H__ diff --git a/app/src/main/jni/vpnproxy-jni/uid_resolver.c b/app/src/main/jni/common/uid_resolver.c similarity index 92% rename from app/src/main/jni/vpnproxy-jni/uid_resolver.c rename to app/src/main/jni/common/uid_resolver.c index ce5d22f4..9f95525e 100644 --- a/app/src/main/jni/vpnproxy-jni/uid_resolver.c +++ b/app/src/main/jni/common/uid_resolver.c @@ -17,8 +17,14 @@ * Copyright 2020-21 - Emanuele Faranda */ -#include "vpnproxy.h" -#include "jni_helpers.h" +#include +#include +#include +#include +#include + +#include "uid_resolver.h" +#include "utils.h" /* ******************************************************* */ @@ -55,7 +61,7 @@ static jint get_uid_proc(int ipver, int ipproto, const char *conn_shex, FILE *fd = fopen(proc, "r"); if (fd == NULL) { - log_android(ANDROID_LOG_ERROR, "fopen(%s) failed[%d]: %s", proc, errno, strerror(errno)); + log_e("fopen(%s) failed[%d]: %s", proc, errno, strerror(errno)); return UID_UNKNOWN; } @@ -75,10 +81,10 @@ static jint get_uid_proc(int ipver, int ipproto, const char *conn_shex, if(!lines++) continue; - //log_android(ANDROID_LOG_INFO, "[try] %s", line); + //log_i("[try] %s", line); if(sscanf(line, fmt, shex, &sport, dhex, &dport, &uid) == 5) { - //log_android(ANDROID_LOG_DEBUG, "[try] %s:%d -> %s:%d [%d]", shex, sport, dhex, dport, uid); + //log_d("[try] %s:%d -> %s:%d [%d]", shex, sport, dhex, dport, uid); if((sport == src_port) && (dport == dst_port) && (!strcmp(conn_dhex, dhex) || !strcmp(dhex, zero)) @@ -145,13 +151,13 @@ static jint get_uid_slow(const zdtun_5tuple_t *conn_info) { sprintf(shex, "%08X%08X%08X%08X", src[0], src[1], src[2], src[3]); sprintf(dhex, "%08X%08X%08X%08X", dst[0], dst[1], dst[2], dst[3]); - //log_android(ANDROID_LOG_INFO, "HEX %s %s", shex, dhex); + //log_i("HEX %s %s", shex, dhex); rv = get_uid_proc(6, conn_info->ipproto, shex, dhex, sport, dport); } //double cpu_time_used = ((double) (clock() - start)) / CLOCKS_PER_SEC; - //log_android(ANDROID_LOG_DEBUG, "cpu_time_used %f", cpu_time_used); + //log_d("cpu_time_used %f", cpu_time_used); return rv; } @@ -209,7 +215,7 @@ uid_resolver_t* init_uid_resolver(jint sdk_version, JNIEnv *env, jobject vpn) { uid_resolver_t *rv = calloc(1, sizeof(uid_resolver_t)); if(!rv) { - log_android(ANDROID_LOG_ERROR, "calloc uid_resolver_t failed"); + log_e("calloc uid_resolver_t failed"); return NULL; } @@ -220,6 +226,10 @@ uid_resolver_t* init_uid_resolver(jint sdk_version, JNIEnv *env, jobject vpn) { return rv; } +uid_resolver_t* init_uid_resolver_from_proc() { + return(init_uid_resolver(0, NULL, 0)); +} + /* ******************************************************* */ void destroy_uid_resolver(uid_resolver_t *resolver) { diff --git a/app/src/main/jni/vpnproxy-jni/uid_resolver.h b/app/src/main/jni/common/uid_resolver.h similarity index 93% rename from app/src/main/jni/vpnproxy-jni/uid_resolver.h rename to app/src/main/jni/common/uid_resolver.h index 01806fcd..3429720c 100644 --- a/app/src/main/jni/vpnproxy-jni/uid_resolver.h +++ b/app/src/main/jni/common/uid_resolver.h @@ -23,9 +23,12 @@ #include #include "zdtun.h" +#define UID_UNKNOWN -1 + typedef struct uid_resolver uid_resolver_t; uid_resolver_t* init_uid_resolver(jint sdk_version, JNIEnv *env, jobject vpn); +uid_resolver_t* init_uid_resolver_from_proc(); void destroy_uid_resolver(uid_resolver_t *resolver); jint get_uid(uid_resolver_t *resolver, const zdtun_5tuple_t *conn_info); diff --git a/app/src/main/jni/vpnproxy-jni/jni_helpers.c b/app/src/main/jni/common/utils.c similarity index 63% rename from app/src/main/jni/vpnproxy-jni/jni_helpers.c rename to app/src/main/jni/common/utils.c index 27241c74..92dd6a37 100644 --- a/app/src/main/jni/vpnproxy-jni/jni_helpers.c +++ b/app/src/main/jni/common/utils.c @@ -14,41 +14,28 @@ * You should have received a copy of the GNU General Public License * along with PCAPdroid. If not, see . * - * Copyright 2020-21 - Emanuele Faranda + * Copyright 2021 - Emanuele Faranda */ -#include -#include "jni_helpers.h" +#include +#include +#include +#include "utils.h" -static int loglevel = 0; -static JNIEnv *cur_env = NULL; -static jclass vpnclass = 0; -static jclass vpn_inst = 0; -static jmethodID reportError = NULL; +int loglevel = 0; +const char *logtag = "VPNProxy"; +void (*logcallback)(int lvl, const char *msg) = NULL; /* ******************************************************* */ -void init_log(int lvl, JNIEnv *env, jclass _vpnclass, jclass _vpn_inst) { +void set_log_level(int lvl) { loglevel = lvl; - cur_env = env; - vpnclass = _vpnclass; - vpn_inst = _vpn_inst; - reportError = jniGetMethodID(cur_env, vpnclass, "reportError", "(Ljava/lang/String;)V"); -} - -/* ******************************************************* */ - -void finish_log() { - cur_env = NULL; - vpnclass = 0; - vpn_inst = 0; - reportError = 0; } /* ******************************************************* */ void log_android(int prio, const char *fmt, ...) { - if (prio >= loglevel) { + if(prio >= loglevel) { char line[1024]; va_list argptr; @@ -56,25 +43,64 @@ void log_android(int prio, const char *fmt, ...) { vsnprintf(line, sizeof(line), fmt, argptr); va_end(argptr); - __android_log_print(prio, "VPNProxy", "%s", line); + __android_log_print(prio, logtag, "%s", line); - if((prio >= ANDROID_LOG_FATAL) && (cur_env != NULL) && (reportError != NULL)) { - // This is a fatal error, report it to the gui - jobject info_string = (*cur_env)->NewStringUTF(cur_env, line); - - if((jniCheckException(cur_env) != 0) || (info_string == NULL)) - return; - - (*cur_env)->CallVoidMethod(cur_env, vpn_inst, reportError, info_string); - jniCheckException(cur_env); - - (*cur_env)->DeleteLocalRef(cur_env, info_string); - } + if(logcallback != NULL) + logcallback(prio, line); } } /* ******************************************************* */ +ssize_t xwrite(int fd, const void *buf, size_t count) { + size_t sofar = 0; + ssize_t ret; + + do { + ret = write(fd, (u_char*)buf + sofar, count - sofar); + + if(ret < 0) { + if(errno == EINTR) + continue; + + return ret; + } + + sofar += ret; + } while((sofar != count) && (ret != 0)); + + if(sofar != count) + return -1; + + return 0; +} + +/* ******************************************************* */ + +ssize_t xread(int fd, void *buf, size_t count) { + size_t sofar = 0; + ssize_t rv; + + do { + rv = read(fd, (char*)buf + sofar, count - sofar); + + if(rv < 0) { + if(errno == EINTR) + continue; + return rv; + } + + sofar += rv; + } while((sofar != count) && (rv != 0)); + + if(sofar != count) + return -1; + + return 0; +} + +/* ******************************************************* */ + int jniCheckException(JNIEnv *env) { jthrowable ex = (*env)->ExceptionOccurred(env); if (ex) { diff --git a/app/src/main/jni/common/utils.h b/app/src/main/jni/common/utils.h new file mode 100644 index 00000000..528dc057 --- /dev/null +++ b/app/src/main/jni/common/utils.h @@ -0,0 +1,45 @@ +/* + * 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 2021 - Emanuele Faranda + */ + +#ifndef __LOG_UTILS_H__ +#define __LOG_UTILS_H__ + +#include +#include +#include + +extern int loglevel; +extern const char* logtag; +extern void (*logcallback)(int lvl, const char *msg); + +#define log_d(...) log_android(ANDROID_LOG_DEBUG, __VA_ARGS__) +#define log_i(...) log_android(ANDROID_LOG_INFO, __VA_ARGS__) +#define log_w(...) log_android(ANDROID_LOG_WARN, __VA_ARGS__) +#define log_e(...) log_android(ANDROID_LOG_ERROR, __VA_ARGS__) +#define log_f(...) log_android(ANDROID_LOG_FATAL, __VA_ARGS__) + +void log_android(int prio, const char *fmt, ...); +ssize_t xwrite(int fd, const void *buf, size_t count); +ssize_t xread(int fd, void *buf, size_t count); + +jclass jniFindClass(JNIEnv *env, const char *name); +jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature); +int jniCheckException(JNIEnv *env); + +#endif // __LOG_UTILS_H__ diff --git a/app/src/main/jni/pcapd/CMakeLists.txt b/app/src/main/jni/pcapd/CMakeLists.txt new file mode 100644 index 00000000..0cdc7cae --- /dev/null +++ b/app/src/main/jni/pcapd/CMakeLists.txt @@ -0,0 +1,36 @@ +project(pcapd C) + +# libpcap +set(LIBPCAP_ROOT ${ROOTDIR}/submodules/libpcap) +add_definitions(-DPACKAGE_VERSION="1.10.1-PRE-GIT") +add_definitions(-DHAVE_STRERROR) +add_definitions(-DHAVE_SOCKLEN_T) +include_directories(${LIBPCAP_ROOT}) + +add_custom_command(OUTPUT ${LIBPCAP_ROOT}/grammar.c ${LIBPCAP_ROOT}/scanner.c + COMMAND cd ${LIBPCAP_ROOT} && ./configure && make grammar.c scanner.c + DEPENDS ${LIBPCAP_ROOT}/grammar.y.in) + +ADD_LIBRARY(pcap STATIC + ${LIBPCAP_ROOT}/pcap.c + ${LIBPCAP_ROOT}/pcap-common.c + ${LIBPCAP_ROOT}/sf-pcap.c + ${LIBPCAP_ROOT}/sf-pcapng.c + ${LIBPCAP_ROOT}/pcap-linux.c + ${LIBPCAP_ROOT}/fad-gifc.c + ${LIBPCAP_ROOT}/nametoaddr.c + ${LIBPCAP_ROOT}/etherent.c + ${LIBPCAP_ROOT}/savefile.c + ${LIBPCAP_ROOT}/fmtutils.c + ${LIBPCAP_ROOT}/gencode.c + ${LIBPCAP_ROOT}/grammar.c + ${LIBPCAP_ROOT}/scanner.c + ${LIBPCAP_ROOT}/bpf_filter.c + ${LIBPCAP_ROOT}/optimize.c + ${LIBPCAP_ROOT}/missing/strlcpy.c) + +# Executables must be names as libraries to be stored into corresponding native folder +add_executable(libpcapd.so pcapd.c nl_utils.c) + +# Better to link static libs to avoid changing the library path +target_link_libraries(libpcapd.so pcap zdtun common) diff --git a/app/src/main/jni/pcapd/nl_utils.c b/app/src/main/jni/pcapd/nl_utils.c new file mode 100644 index 00000000..21b42f1a --- /dev/null +++ b/app/src/main/jni/pcapd/nl_utils.c @@ -0,0 +1,148 @@ +/* + * 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 2021 - Emanuele Faranda + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "nl_utils.h" + +int nl_socket(uint32_t groups) { + struct sockaddr_nl snl; + int sock; + + sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + + if(sock < 0) + return -1; + + memset(&snl, 0, sizeof(snl)); + snl.nl_family = AF_NETLINK; + snl.nl_pid = 0; // managed by the kernel, see man 7 netlink + snl.nl_groups = groups; + + if(bind(sock, (struct sockaddr *)&snl, sizeof(snl)) < 0) { + close(sock); + return -1; + } + + return sock; +} + +/* ******************************************************* */ + +// adapted from libdnet/route-linux.c +int nl_get_route(int af, const addr_t *addr, route_info_t *out) { + static int seq = 0; + struct nlmsghdr *nmsg; + struct rtmsg *rmsg; + struct rtattr *rta; + struct sockaddr_nl snl; + struct iovec iov; + struct msghdr msg; + u_char buf[512]; + int i, alen, ok, nlsock, rv = -1; + + nlsock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + + if(nlsock < 0) + return(-1); + + alen = (af == AF_INET) ? 4 : 16; + memset(buf, 0, sizeof(buf)); + + nmsg = (struct nlmsghdr *)buf; + nmsg->nlmsg_len = NLMSG_LENGTH(sizeof(*nmsg)) + RTA_LENGTH(alen); + nmsg->nlmsg_flags = NLM_F_REQUEST; + nmsg->nlmsg_type = RTM_GETROUTE; + nmsg->nlmsg_seq = ++seq; + + rmsg = (struct rtmsg *)(nmsg + 1); + rmsg->rtm_family = af; + rmsg->rtm_dst_len = alen; + + rta = RTM_RTA(rmsg); + rta->rta_type = RTA_DST; + rta->rta_len = RTA_LENGTH(alen); + + /* XXX - gross hack for default route */ + if (af == AF_INET && (addr->v4 == INADDR_ANY)) { + i = htonl(0x60060606); + memcpy(RTA_DATA(rta), &i, alen); + } else + memcpy(RTA_DATA(rta), &addr, alen); + + memset(&snl, 0, sizeof(snl)); + snl.nl_family = AF_NETLINK; + + iov.iov_base = nmsg; + iov.iov_len = nmsg->nlmsg_len; + + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &snl; + msg.msg_namelen = sizeof(snl); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + if(sendmsg(nlsock, &msg, 0) < 0) + goto out; + + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + + if ((i = recvmsg(nlsock, &msg, 0)) <= 0) + goto out; + + if(nmsg->nlmsg_len < (int)sizeof(*nmsg) || nmsg->nlmsg_len > i || + nmsg->nlmsg_seq != seq) { + errno = EINVAL; + goto out; + } + + if(nmsg->nlmsg_type == NLMSG_ERROR) + goto out; + + i -= NLMSG_LENGTH(sizeof(*nmsg)); + out->gw_len = 0; + + for(rta = RTM_RTA(rmsg); RTA_OK(rta, i); rta = RTA_NEXT(rta, i)) { + if(rta->rta_type == RTA_GATEWAY) { + // may noy be present + memcpy(&out->gateway, RTA_DATA(rta), alen); + out->gw_len = alen; + } else if (rta->rta_type == RTA_OIF) { + rv = 0; + out->ifidx = *(int *) RTA_DATA(rta); + } + } + + if(rv != 0) { + errno = ESRCH; + goto out; + } + +out: + close(nlsock); + + return(rv); +} diff --git a/app/src/main/jni/pcapd/nl_utils.h b/app/src/main/jni/pcapd/nl_utils.h new file mode 100644 index 00000000..d535f0a8 --- /dev/null +++ b/app/src/main/jni/pcapd/nl_utils.h @@ -0,0 +1,41 @@ +/* + * 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 2021 - Emanuele Faranda + */ + +#ifndef __NL_UTILS_H__ +#define __NL_UTILS_H__ + +#include + +typedef struct { + union { + uint32_t v4; + uint8_t v6[8]; + }; +} __attribute__((packed)) addr_t; + +typedef struct { + addr_t gateway; + int ifidx; + int gw_len; +} route_info_t; + +int nl_get_route(int af, const addr_t *addr, route_info_t *out); +int nl_socket(uint32_t groups); + +#endif diff --git a/app/src/main/jni/pcapd/pcapd.c b/app/src/main/jni/pcapd/pcapd.c new file mode 100644 index 00000000..a6c1317f --- /dev/null +++ b/app/src/main/jni/pcapd/pcapd.c @@ -0,0 +1,576 @@ +/* + * 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 2021 - Emanuele Faranda + */ + +/* + * A daemon to capture network packets and send them to a UNIX socket. + * When running as daemon, the PCAPD_PID file is created, storing the the daemon pid. It is + * automatically deleted when the daemon exits. The daemon captures the packets from the internet + * interface (the interface of the default gateway) and detects its changes. + * The daemon expects the PCAPD_SOCKET_PATH UNIX socket to be present. It will connect and + * start dumping the packets to it. When the socket is closed, the daemon exits. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pcapd.h" +#include "nl_utils.h" +#include "common/uid_resolver.h" +#include "common/uid_lru.h" +#include "common/utils.h" +#include "zdtun.h" + +/* ******************************************************* */ + +typedef struct { + int nlsock; + int client; + + pcap_t *pd; + int dlink; + int pf; + int ifidx; + char ifname[IFNAMSIZ]; + uint64_t mac; + uint32_t ip; + + char nlbuf[8192]; /* >= 8192 to avoid truncation, see "man 7 netlink" */ +} pcapd_runtime_t; + +static char errbuf[PCAP_ERRBUF_SIZE]; + +/* ******************************************************* */ + +static uint64_t bytes2mac(const uint8_t *buf) { + uint64_t m = 0; + + memcpy(&m, buf, 6); + + return m; +} + +/* ******************************************************* */ + +static int get_iface_mac(const char *iface, uint64_t *mac) { + struct ifreq ifr; + int fd; + int rv; + + fd = socket(AF_INET, SOCK_DGRAM, 0); + + ifr.ifr_addr.sa_family = AF_INET; + strncpy((char *)ifr.ifr_name, iface, IFNAMSIZ-1); + + if((rv = ioctl(fd, SIOCGIFHWADDR, &ifr)) != -1) + *mac = bytes2mac((uint8_t*)ifr.ifr_hwaddr.sa_data); + + close(fd); + return(rv); +} + +/* ******************************************************* */ + +int get_iface_ip(const char *iface, uint32_t *ip, uint32_t *netmask) { + struct ifreq ifr; + int fd; + int rv; + + fd = socket(AF_INET, SOCK_DGRAM, 0); + + ifr.ifr_addr.sa_family = AF_INET; + strncpy((char *)ifr.ifr_name, iface, IFNAMSIZ-1); + + if((rv = ioctl(fd, SIOCGIFADDR, &ifr)) != -1) { + *ip = ((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr.s_addr; + + if((rv = ioctl(fd, SIOCGIFNETMASK, &ifr)) != -1) + *netmask = ((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr.s_addr; + } + + close(fd); + return(rv); +} + +/* ******************************************************* */ + +static int list_interfaces() { + pcap_if_t *devs, *pd; + + if(pcap_findalldevs(&devs, errbuf) != 0) { + fprintf(stderr, "pcap_findalldevs failed: %s\n", errbuf); + return -1; + } + + for(pd = devs; pd; pd = pd->next) + printf("%s\n", pd->name); + + pcap_freealldevs(devs); + + return 0; +} + +/* ******************************************************* */ + +static void sighandler(__unused int signo) { + log_i("SIGTERM received, terminating"); + unlink(PCAPD_PID); + + exit(0); +} + +/* ******************************************************* */ + +static int create_pid_file() { + FILE *f = fopen(PCAPD_PID, "w"); + + if(!f) + return -1; + + fprintf(f, "%d\n", getpid()); + fclose(f); + + return 0; +} + +/* ******************************************************* */ + +static void finish_pcapd_capture(pcapd_runtime_t *rt) { + if(rt->client) + close(rt->client); + if(rt->pd) + pcap_close(rt->pd); + if(rt->nlsock > 0) + close(rt->nlsock); + + unlink(PCAPD_PID); +} + +/* ******************************************************* */ + +static int init_pcapd_capture(pcapd_runtime_t *rt) { + // daemonize + int pid = fork(); + + if(pid < 0) { + fprintf(stderr, "fork failed[%d]: %s\n", errno, strerror(errno)); + return -1; + } else if(pid != 0) { + // parent + exit(0); + } + + rt->nlsock = -1; + rt->client = -1; + rt->pd = NULL; + rt->pf = -1; + rt->ifidx = -1; + rt->ifname[0] = '\0'; + rt->mac = 0; + rt->ip = 0; + + if(create_pid_file() < 0) { + log_e("pid file creation failed[%d]: %s", errno, strerror(errno)); + finish_pcapd_capture(rt); + return -1; + } + + rt->nlsock = nl_socket(RTMGRP_IPV4_ROUTE | RTMGRP_IPV4_IFADDR | RTMGRP_IPV4_RULE); // TODO IPv6 + + if(rt->nlsock < 0) { + log_e("could not create netlink socket[%d]: %s", errno, strerror(errno)); + finish_pcapd_capture(rt); + return -1; + } + + rt->client = socket(AF_UNIX, SOCK_STREAM, 0); + + if(rt->client < 0) { + log_e("socket creation failed[%d]: %s", errno, strerror(errno)); + finish_pcapd_capture(rt); + return -1; + } + + signal(SIGTERM, &sighandler); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, PCAPD_SOCKET_PATH); + + log_i("Connecting to client..."); + + if(connect(rt->client, (struct sockaddr*) &addr, sizeof(addr)) != 0) { + log_e("client connection failed[%d]: %s", errno, strerror(errno)); + finish_pcapd_capture(rt); + return -1; + } + + log_i("Connected to client"); + unlink(PCAPD_SOCKET_PATH); + + return 0; +} + +/* ******************************************************* */ + +static void check_capture_interface(pcapd_runtime_t *rt) { + addr_t pubaddr = {.v4 = 0x60060606}; // arbitrary IPv4 public address + route_info_t ri = {.ifidx = -1}; + char ifname[IFNAMSIZ]; + + if((nl_get_route(AF_INET, &pubaddr, &ri) < 0) || (ri.ifidx == rt->ifidx)) { + //log_i("check_capture_interface: nope [%s] - %d", rt->ifname, ri.ifidx); + return; + } + + if(if_indextoname(ri.ifidx, ifname) == NULL) { + log_i("could not get ifidx %d ifname", ri.ifidx); + return; + } + + log_i("interface changed [%d -> %d], (re)starting capture", rt->ifidx, ri.ifidx); + + // TODO support larger MTU + pcap_t *pd = pcap_open_live(ifname, 1500, 0, 1, errbuf); + + if(!pd) { + log_i("pcap_open_live(%s) failed: %s", ifname, errbuf); + return; + } + + int dlink = pcap_datalink(pd); + + if((dlink != DLT_EN10MB) && (dlink != DLT_RAW)) { + log_i("[%s] unsupported datalink: %d", ifname, dlink); + pcap_close(pd); + return; + } + + struct bpf_program fcode; + + // Only IP traffic + if(pcap_compile(pd, &fcode, "ip", 1, PCAP_NETMASK_UNKNOWN) < 0) { + log_i("[%s] could not set capture filter: %s", ifname, pcap_geterr(pd)); + pcap_close(pd); + return; + } + + if(pcap_setfilter(pd, &fcode) < 0) { + log_e("[%s] pcap_setfilter failed: %s", ifname, pcap_geterr(pd)); + pcap_freecode(&fcode); + pcap_close(pd); + return; + } + + pcap_freecode(&fcode); + + // Success + if(rt->pd) + pcap_close(rt->pd); + rt->pd = pd; + + rt->mac = 0; + if((dlink == DLT_EN10MB) && (get_iface_mac(ifname, &rt->mac) < 0)) + log_i("Could not get interface %s MAC[%d]: %s", ifname, errno, strerror(errno)); + + uint32_t netmask; + rt->ip = 0; + if(get_iface_ip(ifname, &rt->ip, &netmask) < 0) + log_i("Could not get interface %s IP[%d]: %s", ifname, errno, strerror(errno)); + + rt->dlink = dlink; + rt->pf = pcap_get_selectable_fd(pd); + rt->ifidx = ri.ifidx; + memcpy(rt->ifname, ifname, sizeof(ifname)); + + log_i("Capturing packets from %s", ifname); +} + +/* ******************************************************* */ + +static int handle_nl_message(pcapd_runtime_t *rt) { + struct iovec iov = { + .iov_base = rt->nlbuf, + .iov_len = sizeof(rt->nlbuf) + }; + + struct sockaddr_nl snl; + struct msghdr msg = { + .msg_name = (void *)&snl, + .msg_namelen = sizeof(snl), + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + ssize_t len = recvmsg(rt->nlsock, &msg, 0); + uint8_t found = 0; + + if(len <= 0) { + log_e("netlink recvmsg failed [%d]: %s\n", errno, sizeof(errno)); + return -1; + } + + for(struct nlmsghdr *nh = (struct nlmsghdr *)rt->nlbuf; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) { + uint8_t do_break = 0; + + switch(nh->nlmsg_type) { + case NLMSG_DONE: + do_break = 1; + break; + case RTM_NEWROUTE: + case RTM_NEWRULE: + found = 1; + do_break = 1; + break; + case RTM_NEWADDR: + if(rt->ifidx == ((struct ifaddrmsg *) NLMSG_DATA(nh))->ifa_index) { + log_i("Detected possible IP address change"); + + uint32_t netmask; + if(get_iface_ip(rt->ifname, &rt->ip, &netmask) < 0) + log_i("Could not get interface %s IP[%d]: %s", rt->ifname, errno, strerror(errno)); + + break; + } + break; + } + + if(do_break) + break; + } + + if(found) + check_capture_interface(rt); + + return 0; +} + +/* ******************************************************* */ + +// try to determine the packet direction as it is only available in SLL / SLL2 ("any" interface) +static int is_tx_packet(pcapd_runtime_t *rt, const u_char *pkt, u_int16_t len) { + // TODO check for broadcast / multicast + if((rt->dlink == DLT_EN10MB) && (len >= 14)) { + // Ethernet header present + struct ethhdr *eth = (struct ethhdr *) pkt; + uint64_t smac = bytes2mac(eth->h_source); + uint64_t dmac = bytes2mac(eth->h_dest); + + if(smac != dmac) { + if(smac == rt->mac) + return 1; // TX + else if(dmac == rt->mac) + return 0; // RX + } + + len -= 14; + pkt += 14; + } + + // NOTE: this must be IP traffic due to the PCAP filter + if(len < 20) + return 0; + + struct iphdr *ip = (struct iphdr *) pkt; + if(ip->version != 4) // TODO IPv6 support + return 0; + + if(ip->saddr == rt->ip) + return 1; // TX + + return 0; +} + +/* ******************************************************* */ + +static int run_pcap_dump(int uid_filter) { + int rv = -1; + pcapd_runtime_t rt = {0}; + time_t next_interface_recheck = 0; + uid_resolver_t *resolver = NULL; + uid_lru_t *lru = NULL; + + if(!(resolver = init_uid_resolver_from_proc())) + goto cleanup; + + if(!(lru = uid_lru_init(64))) + goto cleanup; + + if(init_pcapd_capture(&rt) < 0) + goto cleanup; + + check_capture_interface(&rt); + rv = 0; + + while(1) { + struct timeval timeout = {.tv_sec = 1, .tv_usec = 0}; + fd_set fds = {0}; + int maxfd = (rt.client > rt.nlsock) ? rt.client : rt.nlsock; + + FD_SET(rt.client, &fds); + FD_SET(rt.nlsock, &fds); + + if(rt.pf != -1) { + FD_SET(rt.pf, &fds); + maxfd = (maxfd > rt.pf) ? maxfd : rt.pf; + } + + if(select(maxfd + 1, &fds, NULL, NULL, &timeout) < 0) { + log_e("select failed[%d]: %s", errno, strerror(errno)); + rv = -1; + break; + } + + if(FD_ISSET(rt.client, &fds)) { + log_i("client closed"); + break; + } + if(FD_ISSET(rt.nlsock, &fds)) { + if(handle_nl_message(&rt) < 0) { + rv = -1; + break; + } + } + if((rt.pf != -1) && FD_ISSET(rt.pf, &fds)) { + struct pcap_pkthdr *hdr; + const u_char *pkt; + int to_skip = (rt.dlink == DLT_EN10MB) ? 14 : 0; + int rv1 = pcap_next_ex(rt.pd, &hdr, &pkt); + + if(rv1 == PCAP_ERROR) { + log_i("pcap_next_ex failed: %s", pcap_geterr(rt.pd)); + + // Do not abort, just wait for route changes + pcap_close(rt.pd); + rt.pd = NULL; + rt.pf = -1; + rt.ifidx = -1; + rt.ifname[0] = '\0'; + } else if((rv1 == 1) && (hdr->caplen >= to_skip)) { + pcapd_hdr_t phdr; + zdtun_pkt_t zpkt; + uint8_t is_tx = is_tx_packet(&rt, pkt, hdr->caplen); + + pkt += to_skip; + hdr->caplen -= to_skip; + + if(zdtun_parse_pkt((const char*)pkt, hdr->caplen, &zpkt) == 0) { + if(!is_tx) { + // Packet from the internet, swap src and dst + uint16_t tmp = zpkt.tuple.dst_port; + zpkt.tuple.dst_port = zpkt.tuple.src_port; + zpkt.tuple.src_port = tmp; + + zdtun_ip_t tmp1 = zpkt.tuple.dst_ip; + zpkt.tuple.dst_ip = zpkt.tuple.src_ip; + zpkt.tuple.src_ip = tmp1; + } + + int uid = uid_lru_find(lru, &zpkt.tuple); + + if(uid == -2) { + uid = get_uid(resolver, &zpkt.tuple); + uid_lru_add(lru, &zpkt.tuple, uid); + } + + if((uid_filter == -1) || (uid_filter == uid)) { + phdr.ts = hdr->ts; + phdr.len = hdr->caplen; + phdr.uid = uid; + phdr.flags = is_tx ? PCAPD_FLAG_TX : 0; + + // Send the pcapd_hdr_t first, then the packet data. The packet data always starts with + // the IP header. + if((xwrite(rt.client, &phdr, sizeof(phdr)) < 0) || + (xwrite(rt.client, pkt, phdr.len) < 0)) { + log_e("write failed[%d]: %s", errno, strerror(errno)); + rv = -1; + break; + } + } + } + } + } + + if((rt.pd == NULL) && (time(NULL) >= next_interface_recheck)) { + check_capture_interface(&rt); + next_interface_recheck = time(NULL) + 5; + } + } + + log_i("Terminating..."); + +cleanup: + finish_pcapd_capture(&rt); + + if(resolver) + destroy_uid_resolver(resolver); + if(lru) + uid_lru_destroy(lru); + + return rv; +} + +/* ******************************************************* */ + +static void usage() { + fprintf(stderr, "pcapd - root companion for PCAPdroid\n" + "Copyright 2021 Emanuele Faranda \n\n" + "Usage: pcapd [--interfaces|-d]\n" + " --interfaces list the interfaces of the system\n" + " -d [uid] daemonize and dump packets from the internet interface, possibly filtered by uid\n" + ); + + exit(1); +} + +/* ******************************************************* */ + +int main(int argc, char *argv[]) { + logtag = "pcapd"; + + if(argc < 2) + usage(); + + if(!strcmp(argv[1], "--interfaces")) + return list_interfaces(); + else if(!strcmp(argv[1], "-d")) { + int uid_filter = -1; + + if(argc >= 3) + uid_filter = atoi(argv[2]); + + return run_pcap_dump(uid_filter); + } + + usage(); +} diff --git a/app/src/main/jni/pcapd/pcapd.h b/app/src/main/jni/pcapd/pcapd.h new file mode 100644 index 00000000..b6c9d98a --- /dev/null +++ b/app/src/main/jni/pcapd/pcapd.h @@ -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 2021 - Emanuele Faranda + */ + +#ifndef __PCAPD_H__ +#define __PCAPD_H__ + +#define PCAPD_SOCKET_PATH "pcapsock" +#define PCAPD_PID "pcapd.pid" + +#define PCAPD_FLAG_TX (1 << 0) + +#include +#include + +typedef struct { + struct timeval ts; + uid_t uid; + uint16_t len; + uint8_t flags; +} __attribute__((packed)) pcapd_hdr_t; + +#endif \ No newline at end of file diff --git a/app/src/main/jni/vpnproxy-jni/CMakeLists.txt b/app/src/main/jni/vpnproxy-jni/CMakeLists.txt index 9465089e..6cd531cb 100644 --- a/app/src/main/jni/vpnproxy-jni/CMakeLists.txt +++ b/app/src/main/jni/vpnproxy-jni/CMakeLists.txt @@ -1,18 +1,12 @@ -cmake_minimum_required(VERSION 3.10.2) project(vpnproxy-jni) -include(ExternalProject) - -set(CMAKE_VERBOSE_MAKEFILE ON) -set(ROOTDIR ../../../../..) - -include_directories(../../../..) add_library(vpnproxy-jni SHARED vpnproxy.c - uid_resolver.c - jni_helpers.c + capture_proxy.c + capture_root.c ip_lru.c - pcap) + ndpi_master_protos.c + pcap_utils.c) # nDPI set(NDPI_ROOT ${ROOTDIR}/submodules/nDPI) @@ -28,15 +22,10 @@ add_custom_command(OUTPUT ${NDPI_ROOT}/src/include/ndpi_api.h ADD_LIBRARY(ndpi SHARED ${ndpiSources} ${NDPI_ROOT}/src/include/ndpi_api.h) -# zdtun -set(ZDTUN_ROOT ${ROOTDIR}/submodules/zdtun) -include_directories(${ZDTUN_ROOT}) -add_subdirectory(${ZDTUN_ROOT} build) - -# Link find_library(log-lib log) target_link_libraries(vpnproxy-jni zdtun ndpi + common ${log-lib}) diff --git a/app/src/main/jni/vpnproxy-jni/capture_proxy.c b/app/src/main/jni/vpnproxy-jni/capture_proxy.c new file mode 100644 index 00000000..9269b156 --- /dev/null +++ b/app/src/main/jni/vpnproxy-jni/capture_proxy.c @@ -0,0 +1,438 @@ +/* + * 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 2021 - Emanuele Faranda + */ + +#include "vpnproxy.h" +#include "common/utils.h" + +static void protectSocketCallback(zdtun_t *tun, socket_t sock) { + vpnproxy_data_t *proxy = ((vpnproxy_data_t*)zdtun_userdata(tun)); + protectSocket(proxy, sock); +} + +/* ******************************************************* */ + +static bool shouldIgnoreConn(vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple) { +#if 0 + int uid = data.uid; + bool is_unknown_app = ((uid == UID_UNKNOWN) || (uid == 1051 /* netd DNS resolver */)); + + if(((proxy->uid_filter != UID_UNKNOWN) && (proxy->uid_filter != uid)) + && (!is_unknown_app || !proxy->capture_unknown_app_traffic)) + return true; +#endif + + // ignore some internal communications, e.g. DNS-over-TLS check on port 853 + if((tuple->ipver == 4) && (tuple->dst_ip.ip4 == proxy->vpn_dns) && (ntohs(tuple->dst_port) != 53)) + return true; + + return false; +} + +/* ******************************************************* */ + +static void add_known_dns_server(vpnproxy_data_t *proxy, const char *ip) { + ndpi_ip_addr_t parsed; + + if(ndpi_parse_ip_string(ip, &parsed) < 0) { + log_e("ndpi_parse_ip_string(%s) failed", ip); + return; + } + + ndpi_ptree_insert(proxy->known_dns_servers, &parsed, ndpi_is_ipv6(&parsed) ? 128 : 32, 1); +} + +/* ******************************************************* */ + +static int net2tun(zdtun_t *tun, char *pkt_buf, int pkt_size, const zdtun_conn_t *conn_info) { + if(!running) + return 0; + + vpnproxy_data_t *proxy = (vpnproxy_data_t*) zdtun_userdata(tun); + + int rv = write(proxy->tunfd, pkt_buf, pkt_size); + + if(rv < 0) { + if(errno == ENOBUFS) { + char buf[256]; + + // Do not abort, the connection will be terminated + log_e("Got ENOBUFS %s", zdtun_5tuple2str(zdtun_conn_get_5tuple(conn_info), buf, sizeof(buf))); + } else if(errno == EIO) { + log_i("Got I/O error (terminating?)"); + running = false; + } else { + log_f("tun write (%d) failed [%d]: %s", pkt_size, errno, strerror(errno)); + running = false; + } + } else if(rv != pkt_size) { + log_f("partial tun write (%d / %d)", rv, pkt_size); + rv = -1; + } else + rv = 0; + + return rv; +} + +/* ******************************************************* */ + +static void check_socks5_redirection(struct vpnproxy_data *proxy, zdtun_pkt_t *pkt, zdtun_conn_t *conn) { + conn_data_t *data = zdtun_conn_get_userdata(conn); + + if(shouldIgnoreConn(proxy, zdtun_conn_get_5tuple(conn))) + return; + + if((pkt->tuple.ipproto == IPPROTO_TCP) && (((data->sent_pkts + data->rcvd_pkts) == 0))) + zdtun_conn_proxy(conn); +} + +/* ******************************************************* */ + +/* + * If the packet contains a DNS request then rewrite server address + * with public DNS server. Non UDP DNS connections are dropped to block DoH queries which do not + * allow us to extract the requested domain name. + */ +static bool check_dns_req_allowed(struct vpnproxy_data *proxy, zdtun_conn_t *conn) { + const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn); + + if(new_dns_server != 0) { + // Reload DNS server + proxy->dns_server = new_dns_server; + new_dns_server = 0; + + zdtun_ip_t ip = {0}; + ip.ip4 = proxy->dns_server; + zdtun_set_dnat_info(proxy->tun, &ip, htons(53), 4); + + log_d("Using new DNS server"); + } + + bool is_internal_dns = (tuple->ipver == 4) && (tuple->dst_ip.ip4 == proxy->vpn_dns); + bool is_dns_server = is_internal_dns + || ((tuple->ipver == 6) && (memcmp(&tuple->dst_ip.ip6, &proxy->ipv6.dns_server, 16) == 0)); + + if(!is_dns_server) { + // try with known DNS servers + u_int32_t matched = 0; + ndpi_ip_addr_t addr = {0}; + + if(tuple->ipver == 4) + addr.ipv4 = tuple->dst_ip.ip4; + else + memcpy(&addr.ipv6, &tuple->dst_ip.ip6, 16); + + ndpi_ptree_match_addr(proxy->known_dns_servers, &addr, &matched); + + if(matched) { + char ip[INET6_ADDRSTRLEN]; + int family = (tuple->ipver == 4) ? AF_INET : AF_INET6; + + is_dns_server = true; + ip[0] = '\0'; + inet_ntop(family, &tuple->dst_ip, (char *)&ip, sizeof(ip)); + + log_d("Matched known DNS server: %s", ip); + } + } + + if(!is_dns_server) + return(true); + + if((tuple->ipproto == IPPROTO_UDP) && (ntohs(tuple->dst_port) == 53) && (proxy->last_pkt != NULL)) { + zdtun_pkt_t *pkt = proxy->last_pkt; + int dns_length = pkt->l7_len; + + if(dns_length >= sizeof(struct dns_packet)) { + struct dns_packet *dns_data = (struct dns_packet*) pkt->l7; + + if((dns_data->flags & DNS_FLAGS_MASK) != DNS_TYPE_REQUEST) + return(true); + + log_d("Detected DNS query[%u]", dns_length); + proxy->num_dns_requests++; + + if(is_internal_dns) { + /* + * Direct the packet to the public DNS server. Checksum recalculation is not strictly necessary + * here as zdtun will proxy the connection. + */ + zdtun_conn_dnat(conn); + } + + return(true); + } + } + + log_i("blocking packet directed to the DNS server"); + + // block everything else (e.g. DoH) + return(false); +} + +/* ******************************************************* */ + +static int handle_new_connection(zdtun_t *tun, zdtun_conn_t *conn_info) { + vpnproxy_data_t *proxy = ((vpnproxy_data_t *) zdtun_userdata(tun)); + const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn_info); + + if(!check_dns_req_allowed(proxy, conn_info)) { + // block connection + proxy->last_conn_blocked = true; + return (1); + } + + conn_data_t *data = new_connection(proxy, tuple, resolve_uid(proxy, tuple)); + if(!data) { + /* reject connection */ + return (1); + } + + zdtun_conn_set_userdata(conn_info, data); + + if(!shouldIgnoreConn(proxy, tuple)) { + // Important: only set the incr_id on registered connections since + // ConnectionsRegister::connectionsUpdates does not allow gaps + data->incr_id = proxy->incr_id++; + + conns_add(&proxy->new_conns, tuple, data); + } + + /* accept connection */ + return(0); +} + +/* ******************************************************* */ + +static void destroy_connection(zdtun_t *tun, const zdtun_conn_t *conn_info) { + vpnproxy_data_t *proxy = (vpnproxy_data_t*) zdtun_userdata(tun); + conn_data_t *data = zdtun_conn_get_userdata(conn_info); + + if(!data) { + log_e("Missing data in connection"); + return; + } + + const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn_info); + + if(!shouldIgnoreConn(proxy, tuple)) { + // Will free the data in sendConnectionsDump + if(!data->pending_notification) { + // Send last notification + conns_add(&proxy->conns_updates, tuple, data); + } + + data->status = zdtun_conn_get_status(conn_info); + } else + free_connection_data(data); +} + +/* ******************************************************* */ + +static void on_packet(zdtun_t *tun, const char *packet, int size, uint8_t from_tun, const zdtun_conn_t *conn_info) { + conn_data_t *data = zdtun_conn_get_userdata(conn_info); + + if(!data) { + log_e("Missing data in connection"); + return; + } + + vpnproxy_data_t *proxy = ((vpnproxy_data_t*)zdtun_userdata(tun)); + const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn_info); + + data->status = zdtun_conn_get_status(conn_info); + + if(shouldIgnoreConn(proxy, tuple)) { + /* NOTE: account connection stats also for non-matched connections */ + if(from_tun) { + data->sent_pkts++; + data->sent_bytes += size; + } else { + data->rcvd_pkts++; + data->rcvd_bytes += size; + } + + data->last_seen = time(NULL); + + //log_d("Ignoring connection: UID=%d [filter=%d]", data->uid, proxy->uid_filter); + return; + } + + account_packet(proxy, packet, size, from_tun, tuple, data); +} + +/* ******************************************************* */ + +int run_proxy(vpnproxy_data_t *proxy) { + zdtun_t *tun; + char buffer[32768]; + u_int64_t next_purge_ms; + + int flags = fcntl(proxy->tunfd, F_GETFL, 0); + if (flags < 0 || fcntl(proxy->tunfd, F_SETFL, flags & ~O_NONBLOCK) < 0) { + log_f("fcntl ~O_NONBLOCK error [%d]: %s", errno, + strerror(errno)); + return (-1); + } + + zdtun_callbacks_t callbacks = { + .send_client = net2tun, + .account_packet = on_packet, + .on_socket_open = protectSocketCallback, + .on_connection_open = handle_new_connection, + .on_connection_close = destroy_connection, + }; + + // List of known DNS servers + add_known_dns_server(proxy, "8.8.8.8"); + add_known_dns_server(proxy, "8.8.4.4"); + add_known_dns_server(proxy, "1.1.1.1"); + add_known_dns_server(proxy, "1.0.0.1"); + add_known_dns_server(proxy, "2001:4860:4860::8888"); + add_known_dns_server(proxy, "2001:4860:4860::8844"); + add_known_dns_server(proxy, "2606:4700:4700::64"); + add_known_dns_server(proxy, "2606:4700:4700::6400"); + + tun = zdtun_init(&callbacks, proxy); + + if(tun == NULL) { + log_f("zdtun_init failed"); + return(-2); + } + + proxy->tun = tun; + new_dns_server = 0; + + if(proxy->socks5.enabled) { + zdtun_ip_t dnatip = {0}; + dnatip.ip4 = proxy->socks5.proxy_ip; + zdtun_set_socks5_proxy(tun, &dnatip, proxy->socks5.proxy_port, 4); + } + + zdtun_ip_t ip = {0}; + ip.ip4 = proxy->dns_server; + zdtun_set_dnat_info(tun, &ip, ntohs(53), 4); + + refreshTime(proxy); + next_purge_ms = proxy->now_ms + PERIODIC_PURGE_TIMEOUT_MS; + + log_d("Starting packet loop [tunfd=%d]", proxy->tunfd); + + while(running) { + int max_fd; + fd_set fdset; + fd_set wrfds; + int size; + struct timeval timeout = {.tv_sec = 0, .tv_usec = 500*1000}; // wake every 500 ms + + zdtun_fds(tun, &max_fd, &fdset, &wrfds); + + FD_SET(proxy->tunfd, &fdset); + max_fd = max(max_fd, proxy->tunfd); + + if(select(max_fd + 1, &fdset, &wrfds, NULL, &timeout) < 0) { + log_e("select failed[%d]: %s", errno, strerror(errno)); + break; + } + + if(!running) + break; + + refreshTime(proxy); + + if(FD_ISSET(proxy->tunfd, &fdset)) { + /* Packet from VPN */ + size = read(proxy->tunfd, buffer, sizeof(buffer)); + + if (size > 0) { + zdtun_pkt_t pkt; + + if (zdtun_parse_pkt(buffer, size, &pkt) != 0) { + log_d("zdtun_parse_pkt failed"); + goto housekeeping; + } + + proxy->last_pkt = &pkt; + proxy->last_conn_blocked = false; + + if((pkt.tuple.ipver == 6) && (!proxy->ipv6.enabled)) { + char buf[512]; + + log_d("ignoring IPv6 packet: %s", + zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); + goto housekeeping; + } + + // Skip established TCP connections + uint8_t is_tcp_established = ((pkt.tuple.ipproto == IPPROTO_TCP) && + (!(pkt.tcp->th_flags & TH_SYN) || (pkt.tcp->th_flags & TH_ACK))); + + zdtun_conn_t *conn = zdtun_lookup(tun, &pkt.tuple, !is_tcp_established); + + if (!conn) { + if(proxy->last_conn_blocked) { + ; + } else if(!is_tcp_established) { + char buf[512]; + + proxy->num_dropped_connections++; + log_e("zdtun_lookup failed: %s", + zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); + } else { + char buf[512]; + + log_d("skipping established TCP: %s", + zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); + } + goto housekeeping; + } + + if(proxy->socks5.enabled) + check_socks5_redirection(proxy, &pkt, conn); + + if(zdtun_forward(tun, &pkt, conn) != 0) { + char buf[512]; + + log_e("zdtun_forward failed: %s", + zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); + + proxy->num_dropped_connections++; + zdtun_destroy_conn(tun, conn); + goto housekeeping; + } + } else if (size < 0) + log_e("recv(tunfd) returned error [%d]: %s", errno, + strerror(errno)); + } else + zdtun_handle_fd(tun, &fdset, &wrfds); + + + housekeeping: + run_housekeeping(proxy); + + if(proxy->now_ms >= next_purge_ms) { + zdtun_purge_expired(tun, proxy->now_ms / 1000); + next_purge_ms = proxy->now_ms + PERIODIC_PURGE_TIMEOUT_MS; + } + } + + ztdun_finalize(tun); + return(0); +} + + diff --git a/app/src/main/jni/vpnproxy-jni/capture_root.c b/app/src/main/jni/vpnproxy-jni/capture_root.c new file mode 100644 index 00000000..b73cd5ff --- /dev/null +++ b/app/src/main/jni/vpnproxy-jni/capture_root.c @@ -0,0 +1,377 @@ +/* + * 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 2021 - Emanuele Faranda + */ + +#include +#include +#include "vpnproxy.h" +#include "pcapd/pcapd.h" +#include "common/utils.h" +#include "third_party/uthash.h" + +// Keep in sync with zdtun.c +#define ICMP_TIMEOUT_SEC 5 +#define UDP_TIMEOUT_SEC 30 +#define TCP_TIMEOUT_SEC 60 + +/* ******************************************************* */ + +typedef struct { + zdtun_5tuple_t tuple; + conn_data_t *data; + + UT_hash_handle hh; +} pcap_conn_t; + +/* ******************************************************* */ + +static int su_cmd(vpnproxy_data_t *proxy, const char *prog, const char *args) { + char cmd[1024]; + int rv; + JNIEnv *env = proxy->env; + + if(snprintf(cmd, sizeof(cmd), "su -c '%s %s'", prog, args) >= sizeof(cmd)) { + log_e("su cmd buffer too small"); + return -1; + } + + log_d("su_cmd: %s", cmd); + + rv = system(cmd); + + return rv; +} + +/* ******************************************************* */ + +static void get_libprog_path(vpnproxy_data_t *proxy, const char *prog_name, char *buf, int bufsize) { + JNIEnv *env = proxy->env; + jobject prog_str = (*env)->NewStringUTF(env, prog_name); + + buf[0] = '\0'; + + if((prog_str == NULL) || jniCheckException(env)) { + log_e("could not allocate get_libprog_path string"); + return; + } + + jstring obj = (*env)->CallObjectMethod(env, proxy->vpn_service, mids.getLibprogPath, prog_str); + + if(!jniCheckException(env)) { + const char *value = (*env)->GetStringUTFChars(env, obj, 0); + + strncpy(buf, value, bufsize); + buf[bufsize - 1] = '\0'; + + (*env)->ReleaseStringUTFChars(env, obj, value); + } + + (*env)->DeleteLocalRef(env, obj); +} + +/* ******************************************************* */ + +static void kill_pcapd(vpnproxy_data_t *proxy) { + int pid; + char pid_s[8]; + FILE *f = fopen(PCAPD_PID, "r"); + + if(f == NULL) + return; + + fgets(pid_s, sizeof(pid_s), f); + pid = atoi(pid_s); + + if(pid != 0) { + log_d("Killing old pcapd with pid %d", pid); + su_cmd(proxy, "kill", pid_s); + } + + fclose(f); +} + +/* ******************************************************* */ + +static int connectPcapd(vpnproxy_data_t *proxy) { + int sock; + int client = -1; + char workdir[PATH_MAX], pcapd[PATH_MAX]; + + getStringPref(proxy, "getPcapdWorkingDir", workdir, PATH_MAX); + get_libprog_path(proxy, "pcapd", pcapd, sizeof(pcapd)); + + if(!pcapd[0]) + return(-1); + + if(chdir(workdir) < 0) { + log_f("chdir to %s failed [%d]: %s", workdir, + errno, strerror(errno)); + return (-1); + } + + sock = socket(AF_UNIX, SOCK_STREAM, 0); + + if(sock < 0) { + log_f("AF_UNIX socket creation failed [%d]: %s", errno, + strerror(errno)); + return (-1); + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, PCAPD_SOCKET_PATH); + + kill_pcapd(proxy); + unlink(PCAPD_PID); + unlink(PCAPD_SOCKET_PATH); + + if(bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + log_f("AF_UNIX bind failed [%d]: %s", errno, + strerror(errno)); + goto cleanup; + } + + listen(sock, 1); + + log_d("AF_UNIX socket listening at '%s'", addr.sun_path); + + // Start the daemon + char args[32]; + snprintf(args, sizeof(args), "-d %d", proxy->app_filter); + su_cmd(proxy, pcapd, args); + + // Wait for pcapd to start + struct timeval timeout = {.tv_sec = 1, .tv_usec = 0}; + fd_set selfds = {0}; + + FD_SET(sock, &selfds); + select(sock + 1, &selfds, NULL, NULL, &timeout); + + if(!FD_ISSET(sock, &selfds)) { + log_f("pcapd daemon did not spawn"); + goto cleanup; + } + + if((client = accept(sock, NULL, NULL)) < 0) { + log_f("AF_UNIX accept failed [%d]: %s", errno, + strerror(errno)); + goto cleanup; + } + + log_d("Connected to pcapd"); + + unlink(PCAPD_SOCKET_PATH); + + cleanup: + close(sock); + + return client; +} + +/* ******************************************************* */ + +static void handle_packet(vpnproxy_data_t *proxy, pcap_conn_t **connections, pcapd_hdr_t *hdr, const char *buffer) { + zdtun_5tuple_t search; + zdtun_pkt_t pkt; + pcap_conn_t *conn = NULL; + uint8_t from_tun = (hdr->flags & PCAPD_FLAG_TX); + + // NOTE: only IP packets supported + if(zdtun_parse_pkt(buffer, hdr->len, &pkt) != 0) { + log_d("zdtun_parse_pkt failed"); + return; + } + + if(!from_tun) { + // Packet from the internet, swap src and dst + search.ipver = pkt.tuple.ipver; + search.ipproto = pkt.tuple.ipproto; + + search.dst_port = pkt.tuple.src_port; + search.src_port = pkt.tuple.dst_port; + search.dst_ip = pkt.tuple.src_ip; + search.src_ip = pkt.tuple.dst_ip; + } else + search = pkt.tuple; + + HASH_FIND(hh, *connections, &search, sizeof(zdtun_5tuple_t), conn); + + if(!conn) { + conn_data_t *data = new_connection(proxy, &search, hdr->uid); + + if (!data) + return; + + conn = malloc(sizeof(pcap_conn_t)); + + if (!conn) { + log_e("malloc(pcap_conn_t) failed with code %d/%s", + errno, strerror(errno)); + return; + } + + conn->tuple = search; + conn->data = data; + + // TODO read from linux? + data->status = CONN_STATUS_CONNECTED; + + HASH_ADD(hh, *connections, tuple, sizeof(search), conn); + + data->incr_id = proxy->incr_id++; + conns_add(&proxy->new_conns, &search, data); + + switch(conn->tuple.ipproto) { + case IPPROTO_TCP: + proxy->stats.num_tcp_conn++; + proxy->stats.num_tcp_opened++; + break; + case IPPROTO_UDP: + proxy->stats.num_udp_conn++; + proxy->stats.num_udp_opened++; + break; + case IPPROTO_ICMP: + proxy->stats.num_icmp_conn++; + proxy->stats.num_icmp_opened++; + break; + } + } + + account_packet(proxy, buffer, hdr->len, from_tun, &conn->tuple, conn->data); +} + +/* ******************************************************* */ + +static void purge_expired_connections(vpnproxy_data_t *proxy, pcap_conn_t **connections, uint8_t purge_all) { + pcap_conn_t *conn, *tmp; + time_t now = proxy->now_ms / 1000; + + HASH_ITER(hh, *connections, conn, tmp) { + time_t timeout = 0; + + // TODO: sync with linux? + switch(conn->tuple.ipproto) { + case IPPROTO_TCP: + timeout = TCP_TIMEOUT_SEC; + break; + case IPPROTO_UDP: + timeout = UDP_TIMEOUT_SEC; + break; + case IPPROTO_ICMP: + timeout = ICMP_TIMEOUT_SEC; + break; + } + + if(purge_all || (conn->data->status >= CONN_STATUS_CLOSED) || (now >= (timeout + conn->data->last_seen))) { + log_d("IDLE (type=%d)", conn->tuple.ipproto); + + // Will free the data in sendConnectionsDump + if(!conn->data->pending_notification) + conns_add(&proxy->conns_updates, &conn->tuple, conn->data); + + conn->data->status = CONN_STATUS_CLOSED; + + switch (conn->tuple.ipproto) { + case IPPROTO_TCP: + proxy->stats.num_tcp_conn--; + break; + case IPPROTO_UDP: + proxy->stats.num_udp_conn--; + break; + case IPPROTO_ICMP: + proxy->stats.num_icmp_conn--; + break; + } + + HASH_DELETE(hh, *connections, conn); + free(conn); + } + } +} + +/* ******************************************************* */ + +int run_root(vpnproxy_data_t *proxy) { + int sock = connectPcapd(proxy); + int rv = -1; + char buffer[16384]; + pcap_conn_t *connections = NULL; + u_int64_t next_purge_ms; + + if(sock < 0) + return(-1); + + refreshTime(proxy); + next_purge_ms = proxy->now_ms + PERIODIC_PURGE_TIMEOUT_MS; + + log_d("Starting packet loop"); + + while(running) { + pcapd_hdr_t hdr; + fd_set fdset = {0}; + + FD_SET(sock, &fdset); + + struct timeval timeout = {.tv_sec = 0, .tv_usec = 500*1000}; // wake every 500 ms + + if(select(sock + 1, &fdset, NULL, NULL, &timeout) < 0) { + log_e("select failed[%d]: %s", errno, strerror(errno)); + goto cleanup; + } + + refreshTime(proxy); + + if(!running) + break; + + if(!FD_ISSET(sock, &fdset)) + goto housekeeping; + + if(xread(sock, &hdr, sizeof(hdr)) < 0) { + log_e("read hdr from pcapd failed[%d]: %s", errno, strerror(errno)); + goto cleanup; + } + if(hdr.len > sizeof(buffer)) { + log_e("packet too big (%d B)", hdr.len); + goto cleanup; + } + if(xread(sock, buffer, hdr.len) < 0) { + log_e("read %d B packet from pcapd failed[%d]: %s", hdr.len, errno, strerror(errno)); + goto cleanup; + } + + handle_packet(proxy, &connections, &hdr, buffer); + + housekeeping: + run_housekeeping(proxy); + + if(proxy->now_ms >= next_purge_ms) { + purge_expired_connections(proxy, &connections, 0); + next_purge_ms = proxy->now_ms + PERIODIC_PURGE_TIMEOUT_MS; + } + } + + rv = 0; + +cleanup: + purge_expired_connections(proxy, &connections, 1 /* purge_all */); + + close(sock); + return rv; +} \ No newline at end of file diff --git a/app/src/main/jni/vpnproxy-jni/pcap.c b/app/src/main/jni/vpnproxy-jni/pcap_utils.c similarity index 99% rename from app/src/main/jni/vpnproxy-jni/pcap.c rename to app/src/main/jni/vpnproxy-jni/pcap_utils.c index 23bb3386..3eeaa918 100644 --- a/app/src/main/jni/vpnproxy-jni/pcap.c +++ b/app/src/main/jni/vpnproxy-jni/pcap_utils.c @@ -23,7 +23,7 @@ #include #include #include -#include "pcap.h" +#include "pcap_utils.h" /* ******************************************************* */ diff --git a/app/src/main/jni/vpnproxy-jni/pcap.h b/app/src/main/jni/vpnproxy-jni/pcap_utils.h similarity index 100% rename from app/src/main/jni/vpnproxy-jni/pcap.h rename to app/src/main/jni/vpnproxy-jni/pcap_utils.h diff --git a/app/src/main/jni/vpnproxy-jni/utils.c b/app/src/main/jni/vpnproxy-jni/utils.c deleted file mode 100644 index 574845b4..00000000 --- a/app/src/main/jni/vpnproxy-jni/utils.c +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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-21 - Emanuele Faranda - */ - -#include - -/* ******************************************************* */ - -// from DHCPd -static u_int16_t in_cksum(const char *buf, size_t nbytes, u_int32_t sum) { - u_int16_t i; - - /* Checksum all the pairs of bytes first... */ - for (i = 0; i < (nbytes & ~1U); i += 2) { - sum += (u_int16_t) ntohs(*((u_int16_t *)(buf + i))); - /* Add carry. */ - if(sum > 0xFFFF) - sum -= 0xFFFF; - } - - /* If there's a single byte left over, checksum it, too. Network - byte order is big-endian, so the remaining byte is the high byte. */ - if(i < nbytes) { - sum += buf [i] << 8; - /* Add carry. */ - if(sum > 0xFFFF) - sum -= 0xFFFF; - } - - return sum; -} - -/* ******************************************************* */ - -static inline u_int16_t wrapsum(u_int32_t sum) { - sum = ~sum & 0xFFFF; - return htons(sum); -} - -/* ******************************************************* */ - -static u_int16_t ip_checksum(const void *buf, size_t hdr_len) { - return wrapsum(in_cksum(buf, hdr_len, 0)); -} \ No newline at end of file diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.c b/app/src/main/jni/vpnproxy-jni/vpnproxy.c index 0ea18e2f..95d179ba 100644 --- a/app/src/main/jni/vpnproxy-jni/vpnproxy.c +++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.c @@ -17,77 +17,28 @@ * Copyright 2020-21 - Emanuele Faranda */ -#include -#include -#include "utils.c" -#include "ndpi_master_protos.c" -#include "jni_helpers.h" #include "vpnproxy.h" -#include "uid_resolver.h" -#include "pcap.h" +#include "pcap_utils.h" +#include "common/utils.h" #include "ndpi_protocol_ids.h" -#define CAPTURE_STATS_UPDATE_FREQUENCY_MS 300 -#define CONNECTION_DUMP_UPDATE_FREQUENCY_MS 1000 -#define MAX_JAVA_DUMP_DELAY_MS 1000 -#define MAX_DPI_PACKETS 12 -#define MAX_HOST_LRU_SIZE 128 -#define JAVA_PCAP_BUFFER_SIZE (512*1024) // 512K -#define PERIODIC_PURGE_TIMEOUT_MS 5000 - /* ******************************************************* */ -#define DNS_FLAGS_MASK 0x8000 -#define DNS_TYPE_REQUEST 0x0000 -#define DNS_TYPE_RESPONSE 0x8000 +jni_classes_t cls; +jni_methods_t mids; +bool running = false; +uint32_t new_dns_server = 0; -typedef struct dns_packet { - uint16_t transaction_id; - uint16_t flags; - uint16_t questions; - uint16_t answ_rrs; - uint16_t auth_rrs; - uint16_t additional_rrs; - uint8_t initial_dot; // just skip - uint8_t queries[]; -} __attribute__((packed)) dns_packet_t; - -/* ******************************************************* */ - -typedef struct jni_methods { - jmethodID getApplicationByUid; - jmethodID protect; - jmethodID dumpPcapData; - jmethodID sendConnectionsDump; - jmethodID connInit; - jmethodID connSetData; - jmethodID sendServiceStatus; - jmethodID sendStatsDump; - jmethodID statsInit; - jmethodID statsSetData; -} jni_methods_t; - -typedef struct jni_classes { - jclass vpn_service; - jclass conn; - jclass stats; -} jni_classes_t; - -static bool check_dns_req_allowed(zdtun_t *tun, struct vpnproxy_data *proxy, zdtun_conn_t *conn); - -static jni_classes_t cls; -static jni_methods_t mids; -static bool running = false; -static bool dump_vpn_stats_now = false; static bool dump_capture_stats_now = false; static ndpi_protocol_bitmask_struct_t masterProtos; -static uint32_t new_dns_server = 0; /* ******************************************************* */ /* NOTE: these must be reset during each run, as android may reuse the service */ static int dumper_socket; static bool send_header; +static time_t last_connections_dump; +static vpnproxy_data_t *global_proxy = NULL; /* ******************************************************* */ @@ -108,7 +59,7 @@ void free_ndpi(conn_data_t *data) { /* ******************************************************* */ -static void free_connection_data(conn_data_t *data) { +void free_connection_data(conn_data_t *data) { if(!data) return; @@ -125,21 +76,22 @@ static void free_connection_data(conn_data_t *data) { /* ******************************************************* */ -static void conns_add(conn_array_t *arr, const zdtun_conn_t *conn) { +void conns_add(conn_array_t *arr, const zdtun_5tuple_t *tuple, conn_data_t *data) { if(arr->cur_items >= arr->size) { /* Extend array */ arr->size = (arr->size == 0) ? 8 : (arr->size * 2); arr->items = realloc(arr->items, arr->size * sizeof(vpn_conn_t)); if(arr->items == NULL) { - log_android(ANDROID_LOG_FATAL, "realloc(conn_array_t) (%d items) failed", arr->size); + log_e("realloc(conn_array_t) (%d items) failed", arr->size); return; } } vpn_conn_t *slot = &arr->items[arr->cur_items++]; - slot->tuple = *zdtun_conn_get_5tuple(conn); - slot->data = zdtun_conn_get_userdata(conn); + slot->tuple = *tuple; + slot->data = data; + data->pending_notification = true; } /* ******************************************************* */ @@ -163,6 +115,31 @@ static void conns_clear(conn_array_t *arr, bool free_all) { /* ******************************************************* */ +char* getStringPref(vpnproxy_data_t *proxy, const char *key, char *buf, int bufsize) { + JNIEnv *env = proxy->env; + + jmethodID midMethod = jniGetMethodID(env, cls.vpn_service, key, "()Ljava/lang/String;"); + jstring obj = (*env)->CallObjectMethod(env, proxy->vpn_service, midMethod); + char *rv = NULL; + + if(!jniCheckException(env)) { + const char *value = (*env)->GetStringUTFChars(env, obj, 0); + log_d("getStringPref(%s) = %s", key, value); + + strncpy(buf, value, bufsize); + buf[bufsize - 1] = '\0'; + rv = buf; + + (*env)->ReleaseStringUTFChars(env, obj, value); + } + + (*env)->DeleteLocalRef(env, obj); + + return(rv); +} + +/* ******************************************************* */ + static u_int32_t getIPv4Pref(JNIEnv *env, jobject vpn_inst, const char *key) { struct in_addr addr = {0}; @@ -171,10 +148,10 @@ static u_int32_t getIPv4Pref(JNIEnv *env, jobject vpn_inst, const char *key) { if(!jniCheckException(env)) { const char *value = (*env)->GetStringUTFChars(env, obj, 0); - log_android(ANDROID_LOG_DEBUG, "getIPv4Pref(%s) = %s", key, value); + log_d("getIPv4Pref(%s) = %s", key, value); if(inet_aton(value, &addr) == 0) - log_android(ANDROID_LOG_ERROR, "%s() returned invalid IPv4 address", key); + log_e("%s() returned invalid IPv4 address", key); (*env)->ReleaseStringUTFChars(env, obj, value); } @@ -194,10 +171,10 @@ static struct in6_addr getIPv6Pref(JNIEnv *env, jobject vpn_inst, const char *ke if(!jniCheckException(env)) { const char *value = (*env)->GetStringUTFChars(env, obj, 0); - log_android(ANDROID_LOG_DEBUG, "getIPv6Pref(%s) = %s", key, value); + log_d("getIPv6Pref(%s) = %s", key, value); if(inet_pton(AF_INET6, value, &addr) != 1) - log_android(ANDROID_LOG_ERROR, "%s() returned invalid IPv6 address", key); + log_e("%s() returned invalid IPv6 address", key); (*env)->ReleaseStringUTFChars(env, obj, value); } @@ -216,32 +193,13 @@ static jint getIntPref(JNIEnv *env, jobject vpn_inst, const char *key) { value = (*env)->CallIntMethod(env, vpn_inst, midMethod); jniCheckException(env); - log_android(ANDROID_LOG_DEBUG, "getIntPref(%s) = %d", key, value); + log_d("getIntPref(%s) = %d", key, value); return(value); } /* ******************************************************* */ -static void protectSocket(vpnproxy_data_t *proxy, socket_t sock) { - JNIEnv *env = proxy->env; - - /* Call VpnService protect */ - jboolean isProtected = (*env)->CallBooleanMethod( - env, proxy->vpn_service, mids.protect, sock); - jniCheckException(env); - - if (!isProtected) - log_android(ANDROID_LOG_ERROR, "socket protect failed"); -} - -static void protectSocketCallback(zdtun_t *tun, socket_t sock) { - vpnproxy_data_t *proxy = ((vpnproxy_data_t*)zdtun_userdata(tun)); - protectSocket(proxy, sock); -} - -/* ******************************************************* */ - static char* getApplicationByUid(vpnproxy_data_t *proxy, jint uid, char *buf, int bufsize) { JNIEnv *env = proxy->env; const char *value = NULL; @@ -299,9 +257,54 @@ const char *getProtoName(struct ndpi_detection_module_struct *mod, ndpi_protocol /* ******************************************************* */ -static void end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const zdtun_conn_t *conn_info) { - const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn_info); +conn_data_t* new_connection(vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple, int uid) { + conn_data_t *data = calloc(1, sizeof(conn_data_t)); + if(!data) { + log_e("calloc(conn_data_t) failed with code %d/%s", + errno, strerror(errno)); + return(NULL); + } + + /* nDPI */ + if((data->ndpi_flow = calloc(1, SIZEOF_FLOW_STRUCT)) == NULL) { + log_e("ndpi_flow_malloc failed"); + free_ndpi(data); + } + + if((data->src_id = calloc(1, SIZEOF_ID_STRUCT)) == NULL) { + log_e("ndpi_malloc(src_id) failed"); + free_ndpi(data); + } + + if((data->dst_id = calloc(1, SIZEOF_ID_STRUCT)) == NULL) { + log_e("ndpi_malloc(dst_id) failed"); + free_ndpi(data); + } + + data->first_seen = data->last_seen = time(NULL); + data->uid = uid; + + // Try to resolve host name via the LRU cache + zdtun_ip_t ip = tuple->dst_ip; + data->info = ip_lru_find(proxy->ip_to_host, &ip); + + if(data->info) { + char resip[INET6_ADDRSTRLEN]; + int family = (tuple->ipver == 4) ? AF_INET : AF_INET6; + + resip[0] = '\0'; + inet_ntop(family, &ip, resip, sizeof(resip)); + + log_d("Host LRU cache HIT: %s -> %s", resip, data->info); + } + + return(data); +} + +/* ******************************************************* */ + +void end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple) { if(!data->ndpi_flow) return; @@ -315,7 +318,7 @@ static void end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const if(data->l7proto.master_protocol == 0) data->l7proto.master_protocol = data->l7proto.app_protocol; - log_android(ANDROID_LOG_DEBUG, "nDPI completed[ipver=%d, proto=%d] -> l7proto: app=%d, master=%d", + log_d("nDPI completed[ipver=%d, proto=%d] -> l7proto: app=%d, master=%d", tuple->ipver, tuple->ipproto, data->l7proto.app_protocol, data->l7proto.master_protocol); switch (data->l7proto.master_protocol) { @@ -346,7 +349,7 @@ static void end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const rspip[0] = '\0'; inet_ntop(family, &rsp_addr, rspip, sizeof(rspip)); - log_android(ANDROID_LOG_DEBUG, "Host LRU cache ADD [v%d]: %s -> %s", ipver, rspip, data->info); + log_d("Host LRU cache ADD [v%d]: %s -> %s", ipver, rspip, data->info); ip_lru_add(proxy->ip_to_host, &rsp_addr, data->info); } @@ -378,7 +381,7 @@ static void end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const /* ******************************************************* */ -static void process_ndpi_packet(conn_data_t *data, vpnproxy_data_t *proxy, const zdtun_conn_t *conn_info, +static void process_ndpi_packet(conn_data_t *data, vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple, const char *packet, int size, uint8_t from_tun) { bool giveup = ((data->sent_pkts + data->rcvd_pkts) >= MAX_DPI_PACKETS); @@ -389,7 +392,7 @@ static void process_ndpi_packet(conn_data_t *data, vpnproxy_data_t *proxy, const if(giveup || ((data->l7proto.app_protocol != NDPI_PROTOCOL_UNKNOWN) && (!ndpi_extra_dissection_possible(proxy->ndpi, data->ndpi_flow)))) - end_ndpi_detection(data, proxy, conn_info); + end_ndpi_detection(data, proxy, tuple); } /* ******************************************************* */ @@ -397,7 +400,7 @@ static void process_ndpi_packet(conn_data_t *data, vpnproxy_data_t *proxy, const static void javaPcapDump(vpnproxy_data_t *proxy) { JNIEnv *env = proxy->env; - log_android(ANDROID_LOG_DEBUG, "Exporting a %d B PCAP buffer", proxy->java_dump.buffer_idx); + log_d("Exporting a %d B PCAP buffer", proxy->java_dump.buffer_idx); jbyteArray barray = (*env)->NewByteArray(env, proxy->java_dump.buffer_idx); if(jniCheckException(env)) @@ -415,112 +418,7 @@ static void javaPcapDump(vpnproxy_data_t *proxy) { /* ******************************************************* */ -static bool shouldIgnoreConn(vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple, const conn_data_t *data) { -#if 0 - int uid = data.uid; - bool is_unknown_app = ((uid == UID_UNKNOWN) || (uid == 1051 /* netd DNS resolver */)); - - if(((proxy->uid_filter != UID_UNKNOWN) && (proxy->uid_filter != uid)) - && (!is_unknown_app || !proxy->capture_unknown_app_traffic)) - return true; -#endif - - // ignore some internal communications, e.g. DNS-over-TLS check on port 853 - if((tuple->ipver == 4) && (tuple->dst_ip.ip4 == proxy->vpn_dns) && (ntohs(tuple->dst_port) != 53)) - return true; - - return false; -} - -/* ******************************************************* */ - -static void account_packet(zdtun_t *tun, const char *packet, int size, uint8_t from_tun, const zdtun_conn_t *conn_info) { - struct sockaddr_in servaddr = {0}; - conn_data_t *data = zdtun_conn_get_userdata(conn_info); - vpnproxy_data_t *proxy; - - if(!data) { - log_android(ANDROID_LOG_ERROR, "Missing user_data in connection"); - return; - } - - proxy = ((vpnproxy_data_t*)zdtun_userdata(tun)); - -#if 0 - if(from_tun) - log_android(ANDROID_LOG_DEBUG, "tun2net: %ld B", size); - else - log_android(ANDROID_LOG_DEBUG, "net2tun: %lu B", size); -#endif - - /* NOTE: account connection stats also for non-matched connections */ - if(from_tun) { - data->sent_pkts++; - data->sent_bytes += size; - } else { - data->rcvd_pkts++; - data->rcvd_bytes += size; - } - - data->last_seen = time(NULL); - data->status = zdtun_conn_get_status(conn_info); - - if(data->ndpi_flow) - process_ndpi_packet(data, proxy, conn_info, packet, size, from_tun); - - if(shouldIgnoreConn(proxy, zdtun_conn_get_5tuple(conn_info), data)) { - //log_android(ANDROID_LOG_DEBUG, "Ignoring connection: UID=%d [filter=%d]", data->uid, proxy->uid_filter); - return; - } - - if(from_tun) { - proxy->capture_stats.sent_pkts++; - proxy->capture_stats.sent_bytes += size; - } else { - proxy->capture_stats.rcvd_pkts++; - proxy->capture_stats.rcvd_bytes += size; - } - - /* New stats to notify */ - proxy->capture_stats.new_stats = true; - - if(!data->pending_notification) { - conns_add(&proxy->conns_updates, conn_info); - data->pending_notification = true; - } - - if(proxy->java_dump.buffer) { - int tot_size = size + (int) sizeof(pcaprec_hdr_s); - - if((JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx) <= tot_size) { - // Flush the buffer - javaPcapDump(proxy); - } - - if((JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx) <= tot_size) - log_android(ANDROID_LOG_ERROR, "Invalid buffer size [size=%d, idx=%d, tot_size=%d]", JAVA_PCAP_BUFFER_SIZE, proxy->java_dump.buffer_idx, tot_size); - else - proxy->java_dump.buffer_idx += dump_pcap_rec((u_char*)proxy->java_dump.buffer + proxy->java_dump.buffer_idx, (u_char*)packet, size); - } - - if(dumper_socket > 0) { - servaddr.sin_family = AF_INET; - servaddr.sin_port = proxy->pcap_dump.collector_port; - servaddr.sin_addr.s_addr = proxy->pcap_dump.collector_addr; - - if (send_header) { - write_pcap_hdr(dumper_socket, (struct sockaddr *) &servaddr, sizeof(servaddr)); - send_header = false; - } - - write_pcap_rec(dumper_socket, (struct sockaddr *) &servaddr, sizeof(servaddr), - (u_int8_t *) packet, size); - } -} - -/* ******************************************************* */ - -static int resolve_uid(vpnproxy_data_t *proxy, const zdtun_5tuple_t *conn_info) { +int resolve_uid(vpnproxy_data_t *proxy, const zdtun_5tuple_t *conn_info) { char buf[256]; jint uid; @@ -537,10 +435,10 @@ static int resolve_uid(vpnproxy_data_t *proxy, const zdtun_5tuple_t *conn_info) else getApplicationByUid(proxy, uid, appbuf, sizeof(appbuf)); - log_android(ANDROID_LOG_INFO, "%s [%d/%s]", buf, uid, appbuf); + log_i( "%s [%d/%s]", buf, uid, appbuf); } else { uid = UID_UNKNOWN; - log_android(ANDROID_LOG_WARN, "%s => UID not found!", buf); + log_w("%s => UID not found!", buf); } return(uid); @@ -548,230 +446,9 @@ static int resolve_uid(vpnproxy_data_t *proxy, const zdtun_5tuple_t *conn_info) /* ******************************************************* */ -static int handle_new_connection(zdtun_t *tun, zdtun_conn_t *conn_info) { - vpnproxy_data_t *proxy = ((vpnproxy_data_t*)zdtun_userdata(tun)); - const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn_info); - - if(!check_dns_req_allowed(tun, proxy, conn_info)) { - // block connection - proxy->last_conn_blocked = true; - return(1); - } - - conn_data_t *data = calloc(1, sizeof(conn_data_t)); - - if(!data) { - log_android(ANDROID_LOG_ERROR, "calloc(conn_data_t) failed with code %d/%s", - errno, strerror(errno)); - /* reject connection */ - return(1); - } - - /* nDPI */ - if((data->ndpi_flow = calloc(1, SIZEOF_FLOW_STRUCT)) == NULL) { - log_android(ANDROID_LOG_ERROR, "ndpi_flow_malloc failed"); - free_ndpi(data); - } - - if((data->src_id = calloc(1, SIZEOF_ID_STRUCT)) == NULL) { - log_android(ANDROID_LOG_ERROR, "ndpi_malloc(src_id) failed"); - free_ndpi(data); - } - - if((data->dst_id = calloc(1, SIZEOF_ID_STRUCT)) == NULL) { - log_android(ANDROID_LOG_ERROR, "ndpi_malloc(dst_id) failed"); - free_ndpi(data); - } - - data->first_seen = data->last_seen = time(NULL); - data->uid = resolve_uid(proxy, tuple); - - // Try to resolve host name via the LRU cache - zdtun_ip_t ip = tuple->dst_ip; - data->info = ip_lru_find(proxy->ip_to_host, &ip); - - if(data->info) { - char resip[INET6_ADDRSTRLEN]; - int family = (tuple->ipver == 4) ? AF_INET : AF_INET6; - - resip[0] = '\0'; - inet_ntop(family, &ip, resip, sizeof(resip)); - - log_android(ANDROID_LOG_DEBUG, "Host LRU cache HIT: %s -> %s", resip, data->info); - } - - zdtun_conn_set_userdata(conn_info, data); - - if(!shouldIgnoreConn(proxy, tuple, data)) { - // Important: only set the incr_id on registered connections since - // ConnectionsRegister::connectionsUpdates does not allow gaps - data->incr_id = proxy->incr_id++; - - conns_add(&proxy->new_conns, conn_info); - data->pending_notification = true; - } - - /* accept connection */ - return(0); -} - -/* ******************************************************* */ - -static void destroy_connection(zdtun_t *tun, const zdtun_conn_t *conn_info) { - vpnproxy_data_t *proxy = (vpnproxy_data_t*) zdtun_userdata(tun); - conn_data_t *data = zdtun_conn_get_userdata(conn_info); - - if(!data) { - log_android(ANDROID_LOG_ERROR, "Missing user_data in connection"); - return; - } - - /* Will free the other data in sendConnectionsDump */ - end_ndpi_detection(data, proxy, conn_info); - data->status = zdtun_conn_get_status(conn_info); - - if(!data->pending_notification && !shouldIgnoreConn(proxy, zdtun_conn_get_5tuple(conn_info), data)) { - // Send last notification - conns_add(&proxy->conns_updates, conn_info); - data->pending_notification = true; - } -} - -/* ******************************************************* */ - -/* - * If the packet contains a DNS request then rewrite server address - * with public DNS server. Non UDP DNS connections are dropped to block DoH queries which do not - * allow us to extract the requested domain name. - */ -static bool check_dns_req_allowed(zdtun_t *tun, struct vpnproxy_data *proxy, zdtun_conn_t *conn) { - const zdtun_5tuple_t *tuple = zdtun_conn_get_5tuple(conn); - - if(new_dns_server != 0) { - // Reload DNS server - proxy->dns_server = new_dns_server; - new_dns_server = 0; - - zdtun_ip_t ip = {0}; - ip.ip4 = proxy->dns_server; - zdtun_set_dnat_info(tun, &ip, htons(53), 4); - - log_android(ANDROID_LOG_DEBUG, "Using new DNS server"); - } - - bool is_internal_dns = (tuple->ipver == 4) && (tuple->dst_ip.ip4 == proxy->vpn_dns); - bool is_dns_server = is_internal_dns - || ((tuple->ipver == 6) && (memcmp(&tuple->dst_ip.ip6, &proxy->ipv6.dns_server, 16) == 0)); - - if(!is_dns_server) { - // try with known DNS servers - u_int32_t matched = 0; - ndpi_ip_addr_t addr = {0}; - - if(tuple->ipver == 4) - addr.ipv4 = tuple->dst_ip.ip4; - else - memcpy(&addr.ipv6, &tuple->dst_ip.ip6, 16); - - ndpi_ptree_match_addr(proxy->known_dns_servers, &addr, &matched); - - if(matched) { - char ip[INET6_ADDRSTRLEN]; - int family = (tuple->ipver == 4) ? AF_INET : AF_INET6; - - is_dns_server = true; - ip[0] = '\0'; - inet_ntop(family, &tuple->dst_ip, (char *)&ip, sizeof(ip)); - - log_android(ANDROID_LOG_DEBUG, "Matched known DNS server: %s", ip); - } - } - - if(!is_dns_server) - return(true); - - if((tuple->ipproto == IPPROTO_UDP) && (ntohs(tuple->dst_port) == 53) && (proxy->last_pkt != NULL)) { - zdtun_pkt_t *pkt = proxy->last_pkt; - int dns_length = pkt->l7_len; - - if(dns_length >= sizeof(struct dns_packet)) { - struct dns_packet *dns_data = (struct dns_packet*) pkt->l7; - - if((dns_data->flags & DNS_FLAGS_MASK) != DNS_TYPE_REQUEST) - return(true); - - log_android(ANDROID_LOG_DEBUG, "Detected DNS query[%u]", dns_length); - proxy->num_dns_requests++; - - if(is_internal_dns) { - /* - * Direct the packet to the public DNS server. Checksum recalculation is not strictly necessary - * here as zdtun will proxy the connection. - */ - zdtun_conn_dnat(conn); - } - - return(true); - } - } - - log_android(ANDROID_LOG_INFO, "blocking packet directed to the DNS server"); - - // block everything else (e.g. DoH) - return(false); -} - -/* ******************************************************* */ - -static void check_socks5_redirection(zdtun_t *tun, struct vpnproxy_data *proxy, zdtun_pkt_t *pkt, zdtun_conn_t *conn) { - conn_data_t *data = zdtun_conn_get_userdata(conn); - - if(shouldIgnoreConn(proxy, zdtun_conn_get_5tuple(conn), data)) - return; - - if((pkt->tuple.ipproto == IPPROTO_TCP) && (((data->sent_pkts + data->rcvd_pkts) == 0))) - zdtun_conn_proxy(conn); -} - -/* ******************************************************* */ - -static int net2tun(zdtun_t *tun, char *pkt_buf, int pkt_size, const zdtun_conn_t *conn_info) { - if(!running) - return 0; - - vpnproxy_data_t *proxy = (vpnproxy_data_t*) zdtun_userdata(tun); - - int rv = write(proxy->tunfd, pkt_buf, pkt_size); - - if(rv < 0) { - if(errno == ENOBUFS) { - char buf[256]; - - // Do not abort, the connection will be terminated - log_android(ANDROID_LOG_ERROR, "Got ENOBUFS %s", zdtun_5tuple2str(zdtun_conn_get_5tuple(conn_info), buf, sizeof(buf))); - } else if(errno == EIO) { - log_android(ANDROID_LOG_INFO, "Got I/O error (terminating?)"); - running = false; - } else { - log_android(ANDROID_LOG_FATAL, - "tun write (%d) failed [%d]: %s", pkt_size, errno, strerror(errno)); - running = false; - } - } else if(rv != pkt_size) { - log_android(ANDROID_LOG_FATAL, - "partial tun write (%d / %d)", rv, pkt_size); - rv = -1; - } else - rv = 0; - - return rv; -} - -/* ******************************************************* */ - static int dumpConnection(vpnproxy_data_t *proxy, const vpn_conn_t *conn, jobject arr, int idx) { char srcip[INET6_ADDRSTRLEN], dstip[INET6_ADDRSTRLEN]; - struct in_addr addr; + //struct in_addr addr; JNIEnv *env = proxy->env; const zdtun_5tuple_t *conn_info = &conn->tuple; const conn_data_t *data = conn->data; @@ -780,12 +457,12 @@ static int dumpConnection(vpnproxy_data_t *proxy, const vpn_conn_t *conn, jobjec if((inet_ntop(family, &conn_info->src_ip, srcip, sizeof(srcip)) == NULL) || (inet_ntop(family, &conn_info->dst_ip, dstip, sizeof(dstip)) == NULL)) { - log_android(ANDROID_LOG_WARN, "inet_ntop failed: ipver=%d, dstport=%d", conn->tuple.ipver, ntohs(conn_info->dst_port)); + log_w("inet_ntop failed: ipver=%d, dstport=%d", conn->tuple.ipver, ntohs(conn_info->dst_port)); return 0; } #if 0 - log_android(ANDROID_LOG_INFO, "DUMP: [proto=%d]: %s:%u -> %s:%u [%d]", + log_i( "DUMP: [proto=%d]: %s:%u -> %s:%u [%d]", conn_info->ipproto, srcip, ntohs(conn_info->src_port), dstip, ntohs(conn_info->dst_port), @@ -821,7 +498,7 @@ static int dumpConnection(vpnproxy_data_t *proxy, const vpn_conn_t *conn, jobjec (*env)->DeleteLocalRef(env, conn_descriptor); } else { - log_android(ANDROID_LOG_ERROR, "NewObject(ConnectionDescriptor) failed"); + log_e("NewObject(ConnectionDescriptor) failed"); rv = -1; } @@ -835,18 +512,18 @@ static int dumpConnection(vpnproxy_data_t *proxy, const vpn_conn_t *conn, jobjec } /* Perform a full dump of the active connections */ -static void sendConnectionsDump(zdtun_t *tun, vpnproxy_data_t *proxy) { +static void sendConnectionsDump(vpnproxy_data_t *proxy) { if((proxy->new_conns.cur_items == 0) && (proxy->conns_updates.cur_items == 0)) return; - log_android(ANDROID_LOG_DEBUG, "sendConnectionsDump: new=%d, updates=%d", proxy->new_conns.cur_items, proxy->conns_updates.cur_items); + log_d("sendConnectionsDump: new=%d, updates=%d", proxy->new_conns.cur_items, proxy->conns_updates.cur_items); JNIEnv *env = proxy->env; jobject new_conns = (*env)->NewObjectArray(env, proxy->new_conns.cur_items, cls.conn, NULL); jobject conns_updates = (*env)->NewObjectArray(env, proxy->conns_updates.cur_items, cls.conn, NULL); if((new_conns == NULL) || (conns_updates == NULL) || jniCheckException(env)) { - log_android(ANDROID_LOG_ERROR, "NewObjectArray() failed"); + log_e("NewObjectArray() failed"); goto cleanup; } @@ -882,7 +559,7 @@ cleanup: /* ******************************************************* */ -static void sendVPNStats(const vpnproxy_data_t *proxy, const zdtun_statistics_t *stats) { +static void sendStatsDump(const vpnproxy_data_t *proxy, const zdtun_statistics_t *stats) { JNIEnv *env = proxy->env; const capture_stats_t *capstats = &proxy->capture_stats; @@ -892,7 +569,7 @@ static void sendVPNStats(const vpnproxy_data_t *proxy, const zdtun_statistics_t jobject stats_obj = (*env)->NewObject(env, cls.stats, mids.statsInit); if((stats_obj == NULL) || jniCheckException(env)) { - log_android(ANDROID_LOG_ERROR, "NewObject(VPNStats) failed"); + log_e("NewObject(VPNStats) failed"); return; } @@ -926,14 +603,29 @@ static void notifyServiceStatus(vpnproxy_data_t *proxy, const char *status) { /* ******************************************************* */ +void protectSocket(vpnproxy_data_t *proxy, socket_t sock) { + JNIEnv *env = proxy->env; + + if(proxy->root_capture) + return; + + /* Call VpnService protect */ + jboolean isProtected = (*env)->CallBooleanMethod( + env, proxy->vpn_service, mids.protect, sock); + jniCheckException(env); + + if (!isProtected) + log_e("socket protect failed"); +} + +/* ******************************************************* */ + static int connect_dumper(vpnproxy_data_t *proxy) { if(proxy->pcap_dump.enabled) { dumper_socket = socket(AF_INET, proxy->pcap_dump.tcp_socket ? SOCK_STREAM : SOCK_DGRAM, 0); if (!dumper_socket) { - log_android(ANDROID_LOG_FATAL, - "could not open UDP pcap dump socket [%d]: %s", errno, - strerror(errno)); + log_f("could not open UDP pcap dump socket [%d]: %s", errno, strerror(errno)); return(-1); } @@ -946,8 +638,7 @@ static int connect_dumper(vpnproxy_data_t *proxy) { servaddr.sin_addr.s_addr = proxy->pcap_dump.collector_addr; if(connect(dumper_socket, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { - log_android(ANDROID_LOG_FATAL, - "connection to the PCAP receiver failed [%d]: %s", errno, + log_f("connection to the PCAP receiver failed [%d]: %s", errno, strerror(errno)); return(-2); } @@ -959,46 +650,148 @@ static int connect_dumper(vpnproxy_data_t *proxy) { /* ******************************************************* */ -static void add_known_dns_server(vpnproxy_data_t *proxy, const char *ip) { - ndpi_ip_addr_t parsed; +void run_housekeeping(vpnproxy_data_t *proxy) { + if(proxy->capture_stats.new_stats + && ((proxy->now_ms - proxy->capture_stats.last_update_ms) >= CAPTURE_STATS_UPDATE_FREQUENCY_MS) || + dump_capture_stats_now) { + dump_capture_stats_now = false; - if(ndpi_parse_ip_string(ip, &parsed) < 0) { - log_android(ANDROID_LOG_ERROR, "ndpi_parse_ip_string(%s) failed", ip); - return; + if(proxy->tun) + zdtun_get_stats(proxy->tun, &proxy->stats); + + sendStatsDump(proxy, &proxy->stats); + + proxy->capture_stats.new_stats = false; + proxy->capture_stats.last_update_ms = proxy->now_ms; + } else if ((proxy->now_ms - last_connections_dump) >= CONNECTION_DUMP_UPDATE_FREQUENCY_MS) { + sendConnectionsDump(proxy); + last_connections_dump = proxy->now_ms; + } else if ((proxy->java_dump.buffer_idx > 0) + && (proxy->now_ms - proxy->java_dump.last_dump_ms) >= MAX_JAVA_DUMP_DELAY_MS) { + javaPcapDump(proxy); + } +} + +/* ******************************************************* */ + +void refreshTime(vpnproxy_data_t *proxy) { + struct timeval now_tv; + + gettimeofday(&now_tv, NULL); + proxy->now_ms = now_tv.tv_sec * 1000 + now_tv.tv_usec / 1000; +} + +/* ******************************************************* */ + +static void log_callback(int lvl, const char *line) { + if(lvl >= ANDROID_LOG_FATAL) { + vpnproxy_data_t *proxy = global_proxy; + + // This is a fatal error, report it to the gui + jobject info_string = (*proxy->env)->NewStringUTF(proxy->env, line); + + if((jniCheckException(proxy->env) != 0) || (info_string == NULL)) + return; + + (*proxy->env)->CallVoidMethod(proxy->env, proxy->vpn_service, mids.reportError, info_string); + jniCheckException(proxy->env); + + (*proxy->env)->DeleteLocalRef(proxy->env, info_string); + } +} + +/* ******************************************************* */ + +void account_packet(vpnproxy_data_t *proxy, const char *packet, int size, uint8_t from_tun, const zdtun_5tuple_t *tuple, conn_data_t *data) { +#if 0 + if(from_tun) + log_d("tun2net: %ld B", size); + else + log_d("net2tun: %lu B", size); +#endif + + data->last_seen = time(NULL); + + if(from_tun) { + data->sent_pkts++; + data->sent_bytes += size; + proxy->capture_stats.sent_pkts++; + proxy->capture_stats.sent_bytes += size; + } else { + data->rcvd_pkts++; + data->rcvd_bytes += size; + proxy->capture_stats.rcvd_pkts++; + proxy->capture_stats.rcvd_bytes += size; } - ndpi_ptree_insert(proxy->known_dns_servers, &parsed, ndpi_is_ipv6(&parsed) ? 128 : 32, 1); + if(data->ndpi_flow) + process_ndpi_packet(data, proxy, tuple, packet, size, from_tun); + + /* New stats to notify */ + proxy->capture_stats.new_stats = true; + + if (!data->pending_notification) { + conns_add(&proxy->conns_updates, tuple, data); + data->pending_notification = true; + } + + if (proxy->java_dump.buffer) { + int tot_size = size + (int) sizeof(pcaprec_hdr_s); + + if ((JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx) <= tot_size) { +// Flush the buffer + javaPcapDump(proxy); + } + + if ((JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx) <= tot_size) + log_e("Invalid buffer size [size=%d, idx=%d, tot_size=%d]", + JAVA_PCAP_BUFFER_SIZE, proxy->java_dump.buffer_idx, tot_size); + else + proxy->java_dump.buffer_idx += dump_pcap_rec( + (u_char *) proxy->java_dump.buffer + proxy->java_dump.buffer_idx, + (u_char *) packet, size); + } + + if (dumper_socket > 0) { + struct sockaddr_in servaddr = {0}; + servaddr.sin_family = AF_INET; + servaddr.sin_port = proxy->pcap_dump.collector_port; + servaddr.sin_addr.s_addr = proxy->pcap_dump.collector_addr; + + if(send_header) { + write_pcap_hdr(dumper_socket, (struct sockaddr *) &servaddr, sizeof(servaddr)); + send_header = false; + } + + write_pcap_rec(dumper_socket, (struct sockaddr *) &servaddr, sizeof(servaddr), + (u_int8_t *) packet, size); + } } /* ******************************************************* */ static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { - zdtun_t *tun; - char buffer[32767]; - struct timeval now_tv; - u_int64_t now_ms; - u_int64_t next_purge_ms; - time_t last_connections_dump = (time(NULL) * 1000) - CONNECTION_DUMP_UPDATE_FREQUENCY_MS + 1000 /* update in a second */; + last_connections_dump = (time(NULL) * 1000) - CONNECTION_DUMP_UPDATE_FREQUENCY_MS + 1000 /* update in a second */; jclass vpn_class = (*env)->GetObjectClass(env, vpn); - init_log(ANDROID_LOG_DEBUG, env, vpn_class, vpn); - /* Classes */ cls.vpn_service = vpn_class; cls.conn = jniFindClass(env, "com/emanuelef/remote_capture/model/ConnectionDescriptor"); cls.stats = jniFindClass(env, "com/emanuelef/remote_capture/model/VPNStats"); /* Methods */ + mids.reportError = jniGetMethodID(env, vpn_class, "reportError", "(Ljava/lang/String;)V"); mids.getApplicationByUid = jniGetMethodID(env, vpn_class, "getApplicationByUid", "(I)Ljava/lang/String;"), - mids.protect = jniGetMethodID(env, vpn_class, "protect", "(I)Z"); + mids.protect = jniGetMethodID(env, vpn_class, "protect", "(I)Z"); mids.dumpPcapData = jniGetMethodID(env, vpn_class, "dumpPcapData", "([B)V"); mids.sendConnectionsDump = jniGetMethodID(env, vpn_class, "sendConnectionsDump", "([Lcom/emanuelef/remote_capture/model/ConnectionDescriptor;[Lcom/emanuelef/remote_capture/model/ConnectionDescriptor;)V"); mids.sendStatsDump = jniGetMethodID(env, vpn_class, "sendStatsDump", "(Lcom/emanuelef/remote_capture/model/VPNStats;)V"); mids.sendServiceStatus = jniGetMethodID(env, vpn_class, "sendServiceStatus", "(Ljava/lang/String;)V"); + mids.getLibprogPath = jniGetMethodID(env, vpn_class, "getLibprogPath", "(Ljava/lang/String;)Ljava/lang/String;"); mids.connInit = jniGetMethodID(env, cls.conn, "", "()V"); mids.connSetData = jniGetMethodID(env, cls.conn, "setData", /* NOTE: must match ConnectionDescriptor::setData */ - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIIIIJJJJIIII)V"); + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIIIIJJJJIIII)V"); mids.statsInit = jniGetMethodID(env, cls.stats, "", "()V"); mids.statsSetData = jniGetMethodID(env, cls.stats, "setData", "(JJIIIIIIII)V"); @@ -1013,80 +806,48 @@ static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { .vpn_ipv4 = getIPv4Pref(env, vpn, "getVpnIPv4"), .vpn_dns = getIPv4Pref(env, vpn, "getVpnDns"), .dns_server = getIPv4Pref(env, vpn, "getDnsServer"), + .app_filter = getIntPref(env, vpn, "getAppFilterUid"), + .root_capture = (bool) getIntPref(env, vpn, "isRootCapture"), .incr_id = 0, .java_dump = { - .enabled = (bool) getIntPref(env, vpn, "dumpPcapToJava"), + .enabled = (bool) getIntPref(env, vpn, "dumpPcapToJava"), }, .pcap_dump = { - .collector_addr = getIPv4Pref(env, vpn, "getPcapCollectorAddress"), - .collector_port = htons(getIntPref(env, vpn, "getPcapCollectorPort")), - .tcp_socket = false, - .enabled = (bool) getIntPref(env, vpn, "dumpPcapToUdp"), + .collector_addr = getIPv4Pref(env, vpn, "getPcapCollectorAddress"), + .collector_port = htons(getIntPref(env, vpn, "getPcapCollectorPort")), + .tcp_socket = false, + .enabled = (bool) getIntPref(env, vpn, "dumpPcapToUdp"), }, .socks5 = { - .enabled = (bool) getIntPref(env, vpn, "getSocks5Enabled"), - .proxy_ip = getIPv4Pref(env, vpn, "getSocks5ProxyAddress"), - .proxy_port = htons(getIntPref(env, vpn, "getSocks5ProxyPort")), + .enabled = (bool) getIntPref(env, vpn, "getSocks5Enabled"), + .proxy_ip = getIPv4Pref(env, vpn, "getSocks5ProxyAddress"), + .proxy_port = htons(getIntPref(env, vpn, "getSocks5ProxyPort")), }, .ipv6 = { - .enabled = (bool) getIntPref(env, vpn, "getIPv6Enabled"), - .dns_server = getIPv6Pref(env, vpn, "getIpv6DnsServer"), + .enabled = (bool) getIntPref(env, vpn, "getIPv6Enabled"), + .dns_server = getIPv6Pref(env, vpn, "getIpv6DnsServer"), } }; - zdtun_callbacks_t callbacks = { - .send_client = net2tun, - .account_packet = account_packet, - .on_socket_open = protectSocketCallback, - .on_connection_open = handle_new_connection, - .on_connection_close = destroy_connection, - }; - /* Important: init global state every time. Android may reuse the service. */ dumper_socket = -1; send_header = true; running = true; + logcallback = log_callback; + global_proxy = &proxy; + /* nDPI */ proxy.ndpi = init_ndpi(); initMasterProtocolsBitmap(&masterProtos); if(proxy.ndpi == NULL) { - log_android(ANDROID_LOG_FATAL, "nDPI initialization failed"); + log_f("nDPI initialization failed"); return(-1); } - // List of known DNS servers - add_known_dns_server(&proxy, "8.8.8.8"); - add_known_dns_server(&proxy, "8.8.4.4"); - add_known_dns_server(&proxy, "1.1.1.1"); - add_known_dns_server(&proxy, "1.0.0.1"); - add_known_dns_server(&proxy, "2001:4860:4860::8888"); - add_known_dns_server(&proxy, "2001:4860:4860::8844"); - add_known_dns_server(&proxy, "2606:4700:4700::64"); - add_known_dns_server(&proxy, "2606:4700:4700::6400"); - signal(SIGPIPE, SIG_IGN); - // Set blocking - int flags = fcntl(tunfd, F_GETFL, 0); - if (flags < 0 || fcntl(tunfd, F_SETFL, flags & ~O_NONBLOCK) < 0) { - log_android(ANDROID_LOG_FATAL, "fcntl ~O_NONBLOCK error [%d]: %s", errno, - strerror(errno)); - return(-1); - } - - tun = zdtun_init(&callbacks, &proxy); - - if(tun == NULL) { - log_android(ANDROID_LOG_FATAL, "zdtun_init failed"); - return(-2); - } - - log_android(ANDROID_LOG_DEBUG, "Starting packet loop [tunfd=%d]", tunfd); - - notifyServiceStatus(&proxy, "started"); - if(proxy.pcap_dump.enabled) { if(connect_dumper(&proxy) < 0) running = false; @@ -1097,143 +858,21 @@ static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { proxy.java_dump.buffer_idx = 0; if(!proxy.java_dump.buffer) { - log_android(ANDROID_LOG_FATAL, "malloc(java_dump.buffer) failed with code %d/%s", - errno, strerror(errno)); + log_f("malloc(java_dump.buffer) failed with code %d/%s", + errno, strerror(errno)); running = false; } } - zdtun_ip_t ip = {0}; - ip.ip4 = proxy.dns_server; - zdtun_set_dnat_info(tun, &ip, ntohs(53), 4); + memset(&proxy.stats, 0, sizeof(proxy.stats)); - if(proxy.socks5.enabled) { - zdtun_ip_t dnatip = {0}; - dnatip.ip4 = proxy.socks5.proxy_ip; - zdtun_set_socks5_proxy(tun, &dnatip, proxy.socks5.proxy_port, 4); - } + notifyServiceStatus(&proxy, "started"); - new_dns_server = 0; - gettimeofday(&now_tv, NULL); - now_ms = now_tv.tv_sec * 1000 + now_tv.tv_usec / 1000; - next_purge_ms = now_ms + PERIODIC_PURGE_TIMEOUT_MS; + // Run the capture + int rv = proxy.root_capture ? run_root(&proxy) : run_proxy(&proxy); - while(running) { - int max_fd; - fd_set fdset; - fd_set wrfds; - int size; - struct timeval timeout = {.tv_sec = 0, .tv_usec = 500*1000}; // wake every 500 ms + log_d("Stopped packet loop"); - zdtun_fds(tun, &max_fd, &fdset, &wrfds); - - FD_SET(tunfd, &fdset); - max_fd = max(max_fd, tunfd); - - select(max_fd + 1, &fdset, &wrfds, NULL, &timeout); - - if(!running) - break; - - gettimeofday(&now_tv, NULL); - now_ms = now_tv.tv_sec * 1000 + now_tv.tv_usec / 1000; - proxy.now_ms = now_ms; - - if(FD_ISSET(tunfd, &fdset)) { - /* Packet from VPN */ - size = read(tunfd, buffer, sizeof(buffer)); - - if (size > 0) { - zdtun_pkt_t pkt; - int rc; - - if (zdtun_parse_pkt(buffer, size, &pkt) != 0) { - log_android(ANDROID_LOG_DEBUG, "zdtun_parse_pkt failed"); - goto housekeeping; - } - - proxy.last_pkt = &pkt; - proxy.last_conn_blocked = false; - - if((pkt.tuple.ipver == 6) && (!proxy.ipv6.enabled)) { - char buf[512]; - - log_android(ANDROID_LOG_DEBUG, "ignoring IPv6 packet: %s", - zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); - goto housekeeping; - } - - // Skip established TCP connections - uint8_t is_tcp_established = ((pkt.tuple.ipproto == IPPROTO_TCP) && - (!(pkt.tcp->th_flags & TH_SYN) || (pkt.tcp->th_flags & TH_ACK))); - - zdtun_conn_t *conn = zdtun_lookup(tun, &pkt.tuple, !is_tcp_established); - - if (!conn) { - if(proxy.last_conn_blocked) { - ; - } else if(!is_tcp_established) { - char buf[512]; - - proxy.num_dropped_connections++; - log_android(ANDROID_LOG_ERROR, "zdtun_lookup failed: %s", - zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); - } else { - char buf[512]; - - log_android(ANDROID_LOG_DEBUG, "skipping established TCP: %s", - zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); - } - goto housekeeping; - } - - if(proxy.socks5.enabled) - check_socks5_redirection(tun, &proxy, &pkt, conn); - - if((rc = zdtun_forward(tun, &pkt, conn)) != 0) { - char buf[512]; - - log_android(ANDROID_LOG_ERROR, "zdtun_forward failed: %s", - zdtun_5tuple2str(&pkt.tuple, buf, sizeof(buf))); - - proxy.num_dropped_connections++; - zdtun_destroy_conn(tun, conn); - goto housekeeping; - } - } else if (size < 0) - log_android(ANDROID_LOG_ERROR, "recv(tunfd) returned error [%d]: %s", errno, - strerror(errno)); - } else - zdtun_handle_fd(tun, &fdset, &wrfds); - -housekeeping: - - if(proxy.capture_stats.new_stats - && ((now_ms - proxy.capture_stats.last_update_ms) >= CAPTURE_STATS_UPDATE_FREQUENCY_MS) || dump_capture_stats_now) { - zdtun_statistics_t stats; - dump_capture_stats_now = false; - - zdtun_get_stats(tun, &stats); - sendVPNStats(&proxy, &stats); - proxy.capture_stats.new_stats = false; - proxy.capture_stats.last_update_ms = now_ms; - } else if((now_ms - last_connections_dump) >= CONNECTION_DUMP_UPDATE_FREQUENCY_MS) { - sendConnectionsDump(tun, &proxy); - last_connections_dump = now_ms; - } else if((proxy.java_dump.buffer_idx > 0) - && (now_ms - proxy.java_dump.last_dump_ms) >= MAX_JAVA_DUMP_DELAY_MS) { - javaPcapDump(&proxy); - } else if((now_ms >= next_purge_ms) || dump_vpn_stats_now) { - dump_vpn_stats_now = false; - - zdtun_purge_expired(tun, now_ms/1000); - next_purge_ms = now_ms + PERIODIC_PURGE_TIMEOUT_MS; - } - } - - log_android(ANDROID_LOG_DEBUG, "Stopped packet loop"); - - ztdun_finalize(tun); conns_clear(&proxy.new_conns, true); conns_clear(&proxy.conns_updates, true); @@ -1256,11 +895,13 @@ housekeeping: destroy_uid_resolver(proxy.resolver); ndpi_ptree_destroy(proxy.known_dns_servers); - log_android(ANDROID_LOG_DEBUG, "Host LRU cache size: %d", ip_lru_size(proxy.ip_to_host)); + log_d("Host LRU cache size: %d", ip_lru_size(proxy.ip_to_host)); ip_lru_destroy(proxy.ip_to_host); - finish_log(); - return(0); + logcallback = NULL; + global_proxy = NULL; + + return(rv); } /* ******************************************************* */ @@ -1268,7 +909,7 @@ housekeeping: JNIEXPORT void JNICALL Java_com_emanuelef_remote_1capture_CaptureService_stopPacketLoop(JNIEnv *env, jclass type) { /* NOTE: the select on the packets loop uses a timeout to wake up periodically */ - log_android(ANDROID_LOG_INFO, "stopPacketLoop called"); + log_i( "stopPacketLoop called"); running = false; } @@ -1281,10 +922,8 @@ Java_com_emanuelef_remote_1capture_CaptureService_runPacketLoop(JNIEnv *env, jcl JNIEXPORT void JNICALL Java_com_emanuelef_remote_1capture_CaptureService_askStatsDump(JNIEnv *env, jclass clazz) { - if(running) { - dump_vpn_stats_now = true; + if(running) dump_capture_stats_now = true; - } } JNIEXPORT jint JNICALL diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.h b/app/src/main/jni/vpnproxy-jni/vpnproxy.h index 57bc4d09..5f19ca2e 100644 --- a/app/src/main/jni/vpnproxy-jni/vpnproxy.h +++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.h @@ -17,17 +17,27 @@ * Copyright 2020-21 - Emanuele Faranda */ +#ifndef __PCAPDROID_H__ +#define __PCAPDROID_H__ + #include #include #include "zdtun.h" -#include "uid_resolver.h" #include "ip_lru.h" -#include +#include "ndpi_api.h" +#include "common/uid_resolver.h" -#ifndef REMOTE_CAPTURE_VPNPROXY_H -#define REMOTE_CAPTURE_VPNPROXY_H +#define CAPTURE_STATS_UPDATE_FREQUENCY_MS 300 +#define CONNECTION_DUMP_UPDATE_FREQUENCY_MS 1000 +#define MAX_JAVA_DUMP_DELAY_MS 1000 +#define MAX_DPI_PACKETS 12 +#define MAX_HOST_LRU_SIZE 128 +#define JAVA_PCAP_BUFFER_SIZE (512*1024) // 512K +#define PERIODIC_PURGE_TIMEOUT_MS 5000 -#define UID_UNKNOWN -1 +#define DNS_FLAGS_MASK 0x8000 +#define DNS_TYPE_REQUEST 0x0000 +#define DNS_TYPE_RESPONSE 0x8000 typedef struct capture_stats { jlong sent_bytes; @@ -77,6 +87,7 @@ typedef struct vpnproxy_data { jint sdk; JNIEnv *env; jobject vpn_service; + jint app_filter; u_int32_t vpn_dns; u_int32_t dns_server; u_int32_t vpn_ipv4; @@ -90,7 +101,10 @@ typedef struct vpnproxy_data { conn_array_t new_conns; conn_array_t conns_updates; zdtun_pkt_t *last_pkt; + zdtun_t *tun; bool last_conn_blocked; + bool root_capture; + zdtun_statistics_t stats; struct { u_int32_t collector_addr; @@ -120,4 +134,64 @@ typedef struct vpnproxy_data { capture_stats_t capture_stats; } vpnproxy_data_t; -#endif //REMOTE_CAPTURE_H +/* ******************************************************* */ + +typedef struct dns_packet { + uint16_t transaction_id; + uint16_t flags; + uint16_t questions; + uint16_t answ_rrs; + uint16_t auth_rrs; + uint16_t additional_rrs; + uint8_t initial_dot; // just skip + uint8_t queries[]; +} __attribute__((packed)) dns_packet_t; + +/* ******************************************************* */ + +typedef struct jni_methods { + jmethodID reportError; + jmethodID getApplicationByUid; + jmethodID protect; + jmethodID dumpPcapData; + jmethodID sendConnectionsDump; + jmethodID connInit; + jmethodID connSetData; + jmethodID sendServiceStatus; + jmethodID sendStatsDump; + jmethodID statsInit; + jmethodID statsSetData; + jmethodID getLibprogPath; +} jni_methods_t; + +typedef struct jni_classes { + jclass vpn_service; + jclass conn; + jclass stats; +} jni_classes_t; + +/* ******************************************************* */ + +extern jni_methods_t mids; +extern jni_classes_t cls; +extern bool running; +extern uint32_t new_dns_server; +extern bool dump_vpn_stats_now; + +void free_ndpi(conn_data_t *data); +void conns_add(conn_array_t *arr, const zdtun_5tuple_t *tuple, conn_data_t *data); +void end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple); +void run_housekeeping(vpnproxy_data_t *proxy); +void account_packet(vpnproxy_data_t *proxy, const char *packet, int size, uint8_t from_tun, const zdtun_5tuple_t *tuple, conn_data_t *data); +int resolve_uid(vpnproxy_data_t *proxy, const zdtun_5tuple_t *conn_info); +void free_connection_data(conn_data_t *data); +void protectSocket(vpnproxy_data_t *proxy, socket_t sock); +char* getStringPref(vpnproxy_data_t *proxy, const char *key, char *buf, int bufsize); +void refreshTime(vpnproxy_data_t *proxy); +void initMasterProtocolsBitmap(ndpi_protocol_bitmask_struct_t *b); +conn_data_t* new_connection(vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple, int uid); + +int run_proxy(vpnproxy_data_t *proxy); +int run_root(vpnproxy_data_t *proxy); + +#endif //__PCAPDROID_H__ diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml deleted file mode 100644 index 1622f480..00000000 --- a/app/src/main/res/drawable/ic_github.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_money.xml b/app/src/main/res/drawable/ic_money.xml new file mode 100644 index 00000000..24527280 --- /dev/null +++ b/app/src/main/res/drawable/ic_money.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/about_activity.xml b/app/src/main/res/layout/about_activity.xml index 1abebd34..3bb7ec55 100644 --- a/app/src/main/res/layout/about_activity.xml +++ b/app/src/main/res/layout/about_activity.xml @@ -68,5 +68,14 @@ android:layout_alignParentStart="true" android:layout_marginTop="20dp" android:text="@string/gpl_license_link"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_stats.xml b/app/src/main/res/layout/activity_stats.xml index a3b8cf34..c8a68484 100644 --- a/app/src/main/res/layout/activity_stats.xml +++ b/app/src/main/res/layout/activity_stats.xml @@ -140,7 +140,8 @@ android:layout_height="fill_parent"> + android:layout_marginBottom="4dp" + android:id="@+id/dns_server_row"> + android:layout_marginBottom="4dp" + android:id="@+id/dns_queries_row"> + android:layout_marginBottom="4dp" + android:id="@+id/open_sockets_row"> + android:layout_marginBottom="4dp" + android:id="@+id/max_fd_row"> + android:id="@+id/action_donate" + android:title="@string/donate" + android:icon="@drawable/ic_money" /> Chiaro Scuro Tema + Abilita Proxy SOCKS5 + Se abilitato, tutte le connessioni TCP saranno redirette al proxy SOCKS5 configurato. + Proxy + Indiritto IP del Proxy + Porta del Proxy + Cattura come Root + Catturare i pacchetti come root permette a PCAPdroid di funzionare assieme ad altre app VPN. + Donazioni diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 22aa1306..2d85c9e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,5 +125,8 @@ Proxy Proxy IP Address Proxy Port + Capture as Root + Capturing packets as root allows PCAPdroid to run with other VPN apps. + Donate diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 636e9867..53050750 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -43,7 +43,7 @@ app:useSimpleSummaryProvider="true" /> - + - - - + + + + + diff --git a/submodules/libpcap b/submodules/libpcap new file mode 160000 index 00000000..f8e410a8 --- /dev/null +++ b/submodules/libpcap @@ -0,0 +1 @@ +Subproject commit f8e410a8feaa1fb40aa4b833a3fa8af2bd5bca1e diff --git a/submodules/zdtun b/submodules/zdtun index c0d06e49..0196fee9 160000 --- a/submodules/zdtun +++ b/submodules/zdtun @@ -1 +1 @@ -Subproject commit c0d06e4996564c1a7295930148661ac2971aa086 +Subproject commit 0196fee9ff41637da3721c18796eac93eee98b90