Implement runtime captured bytes monitor

This commit is contained in:
emanuele-f 2019-10-25 23:29:31 +02:00
parent 4b67d2b856
commit c4b34757b5
7 changed files with 270 additions and 20 deletions

View File

@ -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));
}

View File

@ -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<List<AppDescriptor>> {
Button mStartButton;
SharedPreferences mPrefs;
TextView mExporterInfo;
TextView mCaptureStatus;
Menu mMenu;
int mFilterUid;
boolean mOpenAppsWhenDone;
List<AppDescriptor> 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<List<AppDescriptor>> loader) {
public void onLoaderReset(@NonNull Loader<List<AppDescriptor>> 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));
}
}

View File

@ -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

View File

@ -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;
/* ******************************************************* */

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="oval" >
<gradient
android:angle="360"
android:startColor="#543456"
android:endColor="#f800f0"
android:type="linear" />
<size android:width="24dp"
android:height="24dp"/>
</shape>
</item>
<item
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp">
<shape android:shape="oval" >
<solid android:color="#fff" />
</shape>
</item>
</layer-list>

View File

@ -1,16 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.emanuelef.remote_capture.MainActivity">
<Button
android:id="@+id/button_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/start_button" />
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="26dp"
android:text="@string/start_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/exporter_info"
</LinearLayout>
app:layout_constraintWidth_default="wrap" />
<TextView
android:id="@+id/exporter_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="92dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_view" />
<TextView
android:id="@+id/status_view"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_marginTop="88dp"
android:background="@drawable/rounded_bg"
android:gravity="center"
android:text="@string/ready"
android:textSize="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,4 +14,7 @@
<string name="ip_address">IP Address</string>
<string name="port">Port</string>
<string name="app_filter">App Filter</string>
<string name="ready">Ready</string>
<string name="stopping">Stopping...</string>
<string name="starting">Starting...</string>
</resources>