From c4b34757b5cb28a8f2b84e36ece5247eb8a748a8 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Fri, 25 Oct 2019 23:29:31 +0200 Subject: [PATCH] Implement runtime captured bytes monitor --- .../remote_capture/CaptureService.java | 30 ++++++ .../remote_capture/MainActivity.java | 92 ++++++++++++++++++- app/src/main/jni/vpnproxy-jni/vpnproxy.c | 84 +++++++++++++++-- app/src/main/jni/vpnproxy-jni/vpnproxy.h | 12 +++ app/src/main/res/drawable/rounded_bg.xml | 25 +++++ app/src/main/res/layout/activity_main.xml | 44 +++++++-- app/src/main/res/values/strings.xml | 3 + 7 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_bg.xml diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java index ae6de6ce..a5363b50 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -29,6 +29,8 @@ import android.os.ParcelFileDescriptor; import android.util.Log; import android.widget.Toast; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import java.io.IOException; import java.net.InetSocketAddress; @@ -45,6 +47,17 @@ public class CaptureService extends VpnService implements Runnable { private int uid_filter; private static CaptureService INSTANCE; + public static final String ACTION_TRAFFIC_STATS_UPDATE = "traffic_stats_update"; + public static final String TRAFFIC_STATS_UPDATE_SENT_BYTES = "sent_bytes"; + public static final String TRAFFIC_STATS_UPDATE_RCVD_BYTES = "rcvd_bytes"; + public static final String TRAFFIC_STATS_UPDATE_SENT_PKTS = "sent_pkts"; + public static final String TRAFFIC_STATS_UPDATE_RCVD_PKTS = "rcvd_pkts"; + + public static final String ACTION_SERVICE_STATUS = "service_status"; + public static final String SERVICE_STATUS_KEY = "status"; + public static final String SERVICE_STATUS_STARTED = "started"; + public static final String SERVICE_STATUS_STOPPED = "stopped"; + static { /* Load native library */ System.loadLibrary("vpnproxy-jni"); @@ -200,6 +213,23 @@ public class CaptureService extends VpnService implements Runnable { return uid; } + public void sendCaptureStats(long sent_bytes, long rcvd_bytes, int sent_pkts, int rcvd_pkts) { + Intent intent = new Intent(ACTION_TRAFFIC_STATS_UPDATE); + + intent.putExtra(TRAFFIC_STATS_UPDATE_SENT_BYTES, sent_bytes); + intent.putExtra(TRAFFIC_STATS_UPDATE_RCVD_BYTES, rcvd_bytes); + intent.putExtra(TRAFFIC_STATS_UPDATE_SENT_PKTS, sent_pkts); + intent.putExtra(TRAFFIC_STATS_UPDATE_RCVD_PKTS, rcvd_pkts); + + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void sendServiceStatus(String cur_status) { + Intent intent = new Intent(ACTION_SERVICE_STATUS); + intent.putExtra(SERVICE_STATUS_KEY, cur_status); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + public String getApplicationByUid(int uid) { return(getPackageManager().getNameForUid(uid)); } diff --git a/app/src/main/java/com/emanuelef/remote_capture/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/MainActivity.java index ec43b75d..99b9de47 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/MainActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/MainActivity.java @@ -19,7 +19,10 @@ package com.emanuelef.remote_capture; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -34,6 +37,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.loader.app.LoaderManager; import androidx.loader.content.AsyncTaskLoader; import androidx.loader.content.Loader; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.PreferenceManager; import android.os.Bundle; @@ -43,6 +47,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.Button; +import android.widget.TextView; import java.util.ArrayList; import java.util.List; @@ -52,12 +57,15 @@ import cat.ereza.customactivityoncrash.config.CaocConfig; public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks> { Button mStartButton; SharedPreferences mPrefs; + TextView mExporterInfo; + TextView mCaptureStatus; Menu mMenu; int mFilterUid; boolean mOpenAppsWhenDone; List mInstalledApps; private static final int REQUEST_CODE_VPN = 2; + private static final int MENU_ITEM_APP_SELECTOR_IDX = 0; public static final int OPERATION_SEARCH_LOADER = 23; private void updateConnectStatus(boolean is_running) { @@ -86,6 +94,16 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mStartButton = findViewById(R.id.button_start); + mExporterInfo = findViewById(R.id.exporter_info); + mCaptureStatus = findViewById(R.id.status_view); + + mPrefs.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { + refreshExporterInfo(); + } + }); + refreshExporterInfo(); updateConnectStatus(CaptureService.isRunning()); @@ -97,19 +115,54 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa if(CaptureService.isRunning()) { CaptureService.stopService(); updateConnectStatus(false); + mStartButton.setEnabled(false); + //mCaptureStatus.setText(R.string.stopping); } else { Intent vpnPrepareIntent = VpnService.prepare(MainActivity.this); if (vpnPrepareIntent != null) { startActivityForResult(vpnPrepareIntent, REQUEST_CODE_VPN); + mStartButton.setEnabled(false); } else { onActivityResult(REQUEST_CODE_VPN, RESULT_OK, null); } updateConnectStatus(true); + mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setEnabled(false); } } }); startLoadingApps(); + + LocalBroadcastManager bcast_man = LocalBroadcastManager.getInstance(this); + + /* Register for stats update */ + bcast_man.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + processStatsUpdateIntent(intent); + } + }, new IntentFilter(CaptureService.ACTION_TRAFFIC_STATS_UPDATE)); + + /* Register for service status */ + bcast_man.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String status = intent.getStringExtra(CaptureService.SERVICE_STATUS_KEY); + + if(status.equals(CaptureService.SERVICE_STATUS_STARTED)) { + mCaptureStatus.setText(formatBytes(0)); + mStartButton.setEnabled(true); + } else if(status.equals(CaptureService.SERVICE_STATUS_STOPPED)) { + mCaptureStatus.setText(R.string.ready); + mStartButton.setEnabled(true); + mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setEnabled(true); + } + } + }, new IntentFilter(CaptureService.ACTION_SERVICE_STATUS)); + } + + private void refreshExporterInfo() { + mExporterInfo.setText("UDP Collector: " + getCollectorIPPref() + ":" + getCollectorPortPref()); } private void startLoadingApps() { @@ -173,7 +226,7 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa public void onSelectedApp(AppDescriptor app) { mFilterUid = app.getUid(); // clone the drawable to avoid a "zoom-in" effect when clicked - mMenu.getItem(0).setIcon(app.getIcon().getConstantState().newDrawable()); + mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setIcon(app.getIcon().getConstantState().newDrawable()); // dismiss the dialog alert.cancel(); @@ -197,6 +250,14 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa return super.onOptionsItemSelected(item); } + private String getCollectorIPPref() { + return(mPrefs.getString(Prefs.PREF_COLLECTOR_IP_KEY, getString(R.string.default_collector_ip))); + } + + private String getCollectorPortPref() { + return(mPrefs.getString(Prefs.PREF_COLLECTOR_PORT_KEY, getString(R.string.default_collector_port))); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -207,14 +268,15 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa // the configuration for the VPN bundle.putString("dns_server", "8.8.8.8"); // TODO: read system DNS - bundle.putString(Prefs.PREF_COLLECTOR_IP_KEY, mPrefs.getString(Prefs.PREF_COLLECTOR_IP_KEY, getString(R.string.default_collector_ip))); - bundle.putInt(Prefs.PREF_COLLECTOR_PORT_KEY, Integer.parseInt(mPrefs.getString(Prefs.PREF_COLLECTOR_PORT_KEY, getString(R.string.default_collector_port)))); + bundle.putString(Prefs.PREF_COLLECTOR_IP_KEY, getCollectorIPPref()); + bundle.putInt(Prefs.PREF_COLLECTOR_PORT_KEY, Integer.parseInt(getCollectorPortPref())); bundle.putInt(Prefs.PREF_UID_FILTER, mFilterUid); intent.putExtra("settings", bundle); Log.d("Main", "onActivityResult -> start CaptureService"); startService(intent); + //mCaptureStatus.setText(R.string.starting); } } @@ -242,7 +304,29 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa } @Override - public void onLoaderReset(@NonNull Loader> loader) { + public void onLoaderReset(@NonNull Loader> loader) {} + public static String formatBytes(long bytes) { + long divisor; + String suffix; + if(bytes < 1024) return bytes + " B"; + + if(bytes < 1024*1024) { divisor = 1024; suffix = "KB"; } + else if(bytes < 1024*1024*1024) { divisor = 1024*1024; suffix = "MB"; } + else { divisor = 1024*1024*1024; suffix = "GB"; } + + return String.format("%.1f %s", ((float)bytes) / divisor, suffix); + } + + public void processStatsUpdateIntent(Intent intent) { + long bytes_sent = intent.getLongExtra(CaptureService.TRAFFIC_STATS_UPDATE_SENT_BYTES, 0); + long bytes_rcvd= intent.getLongExtra(CaptureService.TRAFFIC_STATS_UPDATE_RCVD_BYTES, 0); + int pkts_sent = intent.getIntExtra(CaptureService.TRAFFIC_STATS_UPDATE_SENT_PKTS, 0); + int pkts_rcvd = intent.getIntExtra(CaptureService.TRAFFIC_STATS_UPDATE_RCVD_PKTS, 0); + + Log.w("MainReceiver", "Got StatsUpdate: bytes_sent=" + bytes_sent + ", bytes_rcvd=" + + bytes_rcvd + ", pkts_sent=" + pkts_sent + ", pkts_rcvd=" + pkts_rcvd); + + mCaptureStatus.setText(formatBytes(bytes_sent + bytes_rcvd)); } } diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.c b/app/src/main/jni/vpnproxy-jni/vpnproxy.c index b6b8eed0..9d558e7c 100644 --- a/app/src/main/jni/vpnproxy-jni/vpnproxy.c +++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.c @@ -23,6 +23,7 @@ #include "pcap.h" #define VPN_TAG "VPNProxy" +#define CAPTURE_STATS_UPDATE_FREQUENCY_MS 300 /* ******************************************************* */ @@ -178,7 +179,7 @@ static char* getApplicationByUid(vpnproxy_data_t *proxy, int uid, char *buf, siz static int dumper_socket = -1; -static void dump_packet_to_udp(char *packet, size_t size, bool from_tap, const zdtun_conn_t *conn_info, struct vpnproxy_data *proxy) { +static void process_packet_info(char *packet, size_t size, bool from_tap, const zdtun_conn_t *conn_info, struct vpnproxy_data *proxy) { struct sockaddr_in servaddr = {0}; bool send_header = false; int uid = (int)conn_info->user_data; @@ -197,6 +198,20 @@ static void dump_packet_to_udp(char *packet, size_t size, bool from_tap, const z return; } + if(from_tap) { + proxy->capture_stats.rcvd_pkts++; + proxy->capture_stats.rcvd_bytes += size; + } else { + proxy->capture_stats.sent_pkts++; + proxy->capture_stats.sent_bytes += size; + } + + /* New stats to notify */ + proxy->capture_stats.new_stats = true; + + if(!proxy->pcap_dump.enabled) + return; + #if 1 if(dumper_socket <= 0) { dumper_socket = socket(AF_INET, SOCK_DGRAM, 0); @@ -355,8 +370,7 @@ static int net2tap(zdtun_t *tun, char *pkt_buf, ssize_t pkt_size, const zdtun_co check_dns(proxy, pkt_buf, pkt_size, 0 /* reply */); - if(proxy->pcap_dump.enabled) - dump_packet_to_udp(pkt_buf, pkt_size, false, conn_info, proxy); + process_packet_info(pkt_buf, pkt_size, false, conn_info, proxy); // TODO return value check write(proxy->tapfd, pkt_buf, pkt_size); @@ -365,6 +379,42 @@ static int net2tap(zdtun_t *tun, char *pkt_buf, ssize_t pkt_size, const zdtun_co /* ******************************************************* */ +static void sendCaptureStats(vpnproxy_data_t *proxy) { + JNIEnv *env = proxy->env; + capture_stats_t *stats = &proxy->capture_stats; + jclass vpn_service_cls = (*env)->GetObjectClass(env, proxy->vpn_service); + const char *value = NULL; + + jmethodID midMethod = (*env)->GetMethodID(env, vpn_service_cls, "sendCaptureStats", "(JJII)V"); + if(!midMethod) + __android_log_print(ANDROID_LOG_FATAL, VPN_TAG, "GetMethodID(sendCaptureStats) failed"); + + (*env)->CallObjectMethod(env, proxy->vpn_service, midMethod, stats->sent_bytes, stats->rcvd_bytes, + stats->sent_pkts, stats->rcvd_pkts); +} + +/* ******************************************************* */ + +static void notifyServiceStatus(vpnproxy_data_t *proxy, const char *status) { + JNIEnv *env = proxy->env; + capture_stats_t *stats = &proxy->capture_stats; + jclass vpn_service_cls = (*env)->GetObjectClass(env, proxy->vpn_service); + const char *value = NULL; + jstring status_str; + + jmethodID midMethod = (*env)->GetMethodID(env, vpn_service_cls, "sendServiceStatus", "(Ljava/lang/String;)V"); + if(!midMethod) + __android_log_print(ANDROID_LOG_FATAL, VPN_TAG, "GetMethodID(sendServiceStatus) failed"); + + status_str = (*env)->NewStringUTF(env, status); + + (*env)->CallObjectMethod(env, proxy->vpn_service, midMethod, status_str); + + (*env)->DeleteLocalRef(env, status_str); +} + +/* ******************************************************* */ + static int running = 0; static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) { @@ -412,39 +462,52 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) { __android_log_print(ANDROID_LOG_DEBUG, VPN_TAG, "Starting packet loop [tapfd=%d]", tapfd); - // TODO wake via select event + notifyServiceStatus(&proxy, "started"); + while(running) { int max_fd; fd_set fdset; fd_set wrfds; ssize_t size; + u_int64_t now_ms; + struct timeval now_tv; + struct timeval timeout = {.tv_sec = 0, .tv_usec = 500*1000}; // wake every 500 ms zdtun_fds(tun, &max_fd, &fdset, &wrfds); FD_SET(tapfd, &fdset); max_fd = max(max_fd, tapfd); - select(max_fd + 1, &fdset, &wrfds, NULL, NULL); + 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; + if(FD_ISSET(tapfd, &fdset)) { /* Packet from VPN */ size = read(tapfd, buffer, sizeof(buffer)); - const zdtun_conn_t conn_info; if(size > 0) { + zdtun_conn_t conn_info; proxy.dns_changed = check_dns(&proxy, buffer, size, 1 /* query */) != 0; - if(proxy.pcap_dump.enabled) - dump_packet_to_udp(buffer, size, true, &conn_info, &proxy); - zdtun_forward(tun, buffer, size, &conn_info); + + process_packet_info(buffer, size, true, &conn_info, &proxy); } else if (size < 0) __android_log_print(ANDROID_LOG_ERROR, VPN_TAG, "recv(tapfd) returned error [%d]: %s", errno, strerror(errno)); } else zdtun_handle_fd(tun, &fdset, &wrfds); + + if(proxy.capture_stats.new_stats + && ((now_ms - proxy.capture_stats.last_update_ms) >= CAPTURE_STATS_UPDATE_FREQUENCY_MS)) { + sendCaptureStats(&proxy); + proxy.capture_stats.new_stats = false; + proxy.capture_stats.last_update_ms = now_ms; + } } __android_log_print(ANDROID_LOG_DEBUG, VPN_TAG, "Stopped packet loop"); @@ -456,6 +519,7 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) { dumper_socket = -1; } + notifyServiceStatus(&proxy, "stopped"); return(0); } @@ -463,8 +527,8 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) { 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 */ running = 0; - /* TODO wake the possibly sleeping thread */ } JNIEXPORT void JNICALL diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.h b/app/src/main/jni/vpnproxy-jni/vpnproxy.h index 64eb3d83..aef129a4 100644 --- a/app/src/main/jni/vpnproxy-jni/vpnproxy.h +++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.h @@ -24,6 +24,16 @@ #ifndef REMOTE_CAPTURE_VPNPROXY_H #define REMOTE_CAPTURE_VPNPROXY_H +typedef struct capture_stats { + u_int64_t sent_bytes; + u_int64_t rcvd_bytes; + u_int32_t sent_pkts; + u_int32_t rcvd_pkts; + + bool new_stats; + u_int64_t last_update_ms; +} capture_stats_t; + typedef struct vpnproxy_data { int tapfd; jint sdk; @@ -41,6 +51,8 @@ typedef struct vpnproxy_data { int uid_filter; bool enabled; } pcap_dump; + + capture_stats_t capture_stats; } vpnproxy_data_t; /* ******************************************************* */ diff --git a/app/src/main/res/drawable/rounded_bg.xml b/app/src/main/res/drawable/rounded_bg.xml new file mode 100644 index 00000000..b749ea2d --- /dev/null +++ b/app/src/main/res/drawable/rounded_bg.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 36f4b2d3..11b67943 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,48 @@ -