diff --git a/app/build.gradle b/app/build.gradle
index 69036fe1..c8c3ffb4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -37,4 +37,5 @@ dependencies {
implementation project(path: ':zdtun')
implementation 'cat.ereza:customactivityoncrash:2.2.0'
implementation project(path: ':ndpi')
+ implementation 'org.nanohttpd:nanohttpd:2.3.1'
}
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 080092e8..57455b34 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java
@@ -21,6 +21,7 @@ package com.emanuelef.remote_capture;
import android.annotation.TargetApi;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.VpnService;
import android.os.Build;
@@ -30,6 +31,7 @@ import android.util.Log;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.preference.PreferenceManager;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -43,11 +45,14 @@ public class CaptureService extends VpnService implements Runnable {
private String vpn_dns;
private String public_dns;
private String collector_address;
+ private Prefs.DumpMode dump_mode;
private boolean capture_unknown_app_traffic;
private int collector_port;
+ private int http_server_port;
private int uid_filter;
private long last_bytes;
private static CaptureService INSTANCE;
+ private HTTPServer mHttpServer;
public static final String ACTION_TRAFFIC_STATS_UPDATE = "traffic_stats_update";
public static final String ACTION_CONNECTIONS_DUMP = "connections_dump";
@@ -82,19 +87,38 @@ public class CaptureService extends VpnService implements Runnable {
Log.d(CaptureService.TAG, "onStartCommand");
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Bundle settings = intent.getBundleExtra("settings");
-
- // retrieve settings
assert settings != null;
- public_dns = settings.getString("dns_server");
+
+ // Retrieve Configuration
+ public_dns = Utils.getDnsServer(getApplicationContext());
vpn_dns = "10.215.173.2";
vpn_ipv4 = "10.215.173.1";
- collector_address = settings.getString(Prefs.PREF_COLLECTOR_IP_KEY);
- collector_port = settings.getInt(Prefs.PREF_COLLECTOR_PORT_KEY);;
uid_filter = settings.getInt(Prefs.PREF_UID_FILTER);
- capture_unknown_app_traffic = settings.getBoolean(Prefs.PREF_CAPTURE_UNKNOWN_APP_TRAFFIC);
+
+ collector_address = Prefs.getCollectorIp(prefs);
+ collector_port = Prefs.getCollectorPort(prefs);
+ http_server_port = Prefs.getHttpServerPort(prefs);
+ capture_unknown_app_traffic = Prefs.getCaptureUnknownAppTraffic(prefs);
+ dump_mode = Prefs.getDumpMode(prefs);
last_bytes = 0;
+ if(dump_mode == Prefs.DumpMode.HTTP_SERVER) {
+ if (mHttpServer == null)
+ mHttpServer = new HTTPServer(getApplicationContext(), http_server_port);
+
+ try {
+ mHttpServer.startConnections();
+ } catch (IOException e) {
+ Log.e(CaptureService.TAG, "Could not start the HTTP server");
+ e.printStackTrace();
+ }
+ } else
+ mHttpServer = null;
+
+ Log.i("Main", "Using DNS server " + public_dns);
+
// VPN
/* In order to see the DNS packets into the VPN we must set an internal address as the DNS
* server. */
@@ -139,6 +163,9 @@ public class CaptureService extends VpnService implements Runnable {
if(mThread != null) {
mThread.interrupt();
}
+ if(mHttpServer != null)
+ mHttpServer.stop();
+
super.onDestroy();
}
@@ -151,6 +178,10 @@ public class CaptureService extends VpnService implements Runnable {
}
mParcelFileDescriptor = null;
}
+
+ if(mHttpServer != null)
+ mHttpServer.endConnections();
+ // NOTE: do not destroy the mHttpServer, let it terminate the active connections
}
/* Check if the VPN service was launched */
@@ -175,6 +206,14 @@ public class CaptureService extends VpnService implements Runnable {
return((INSTANCE != null) ? INSTANCE.collector_port : 0);
}
+ public static int getHTTPServerPort() {
+ return((INSTANCE != null) ? INSTANCE.http_server_port : 0);
+ }
+
+ public static Prefs.DumpMode getDumpMode() {
+ return((INSTANCE != null) ? INSTANCE.dump_mode : Prefs.DumpMode.NONE);
+ }
+
/* Stop a running VPN service */
public static void stopService() {
if (INSTANCE != null) {
@@ -218,6 +257,15 @@ public class CaptureService extends VpnService implements Runnable {
return(capture_unknown_app_traffic ? 1 : 0);
}
+ // returns 1 if dumpPcapData should be called
+ public int dumpPcapToJava() {
+ return((mHttpServer != null) ? 1 : 0);
+ }
+
+ public int dumpPcapToUdp() {
+ return((dump_mode == Prefs.DumpMode.UDP_EXPORTER) ? 1 : 0);
+ }
+
// from NetGuard
@TargetApi(Build.VERSION_CODES.Q)
public int getUidQ(int version, int protocol, String saddr, int sport, String daddr, int dport) {
@@ -271,6 +319,12 @@ public class CaptureService extends VpnService implements Runnable {
return(getPackageManager().getNameForUid(uid));
}
+ /* Exports a PCAP data chunk */
+ public void dumpPcapData(byte[] data) {
+ if(mHttpServer != null)
+ mHttpServer.pushData(data);
+ }
+
public static native void runPacketLoop(int fd, CaptureService vpn, int sdk);
public static native void stopPacketLoop();
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/ChunkedInputStream.java b/app/src/main/java/com/emanuelef/remote_capture/ChunkedInputStream.java
new file mode 100644
index 00000000..7cd08887
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/ChunkedInputStream.java
@@ -0,0 +1,135 @@
+/*
+ This file is part of RemoteCapture.
+
+ RemoteCapture 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.
+
+ RemoteCapture 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 RemoteCapture. If not, see .
+
+ Copyright 2020 by Emanuele Faranda
+*/
+
+package com.emanuelef.remote_capture;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/* Provides an input stream to read data from bytes chunks produced
+ asynchronously via produceData(). bytes[] chunks are used instead of a
+ single bytes[] in order to avoid excessive data copies.
+ */
+class ChunkedInputStream extends InputStream {
+ private static final byte[] pcapHeader = Utils.hexStringToByteArray("d4c3b2a1020004000000000000000000ffff000065000000");
+ final Lock mLock = new ReentrantLock();
+ final Condition newData = mLock.newCondition();
+ ArrayList mChunks = new ArrayList();
+ int mCurChunkIndex = 0;
+ boolean hasFinished = false;
+
+ ChunkedInputStream() {
+ // Send the PCAP header as the first chunk
+ mChunks.add(pcapHeader);
+ }
+
+ /* Mark the termination of stream */
+ public void stop() {
+ mLock.lock();
+
+ try {
+ hasFinished = true;
+ newData.signal();
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ /* Produce data to be read from the stream */
+ public void produceData(byte data[]) {
+ mLock.lock();
+ try {
+ if(hasFinished)
+ return;
+
+ mChunks.add(data);
+ newData.signal();
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ @Override
+ public int read(byte[] buf, int off, int maxlen) {
+ int out_size = 0;
+
+ if(maxlen <= 0)
+ return(0);
+
+ mLock.lock();
+ try {
+ /* Possibly wait for new data */
+ while((!hasFinished) && (mChunks.size() == 0))
+ newData.await();
+
+ if(mChunks.size() > 0) {
+ /* At least one byte will be returned here. Do not call await() below,
+ just return the available bytes to provide a more responsive transfer. */
+
+ while((mChunks.size() > 0) && (maxlen > 0)) {
+ byte[] chunk = mChunks.get(0);
+
+ if(off > 0) {
+ // skip bytes due to the offset
+ int toSkip = Math.min(off, chunk.length - mCurChunkIndex);
+ off -= toSkip;
+ mCurChunkIndex += toSkip;
+ }
+
+ if (mCurChunkIndex < chunk.length) {
+ int copy_length = Math.min(maxlen, chunk.length - mCurChunkIndex);
+ System.arraycopy(chunk, mCurChunkIndex, buf, out_size, copy_length);
+ out_size += copy_length;
+ mCurChunkIndex += copy_length;
+ maxlen -= copy_length;
+ }
+
+ if (mCurChunkIndex >= chunk.length) {
+ // next chunk
+ mChunks.remove(0);
+ mCurChunkIndex = 0;
+ }
+ }
+
+ return(out_size);
+ }
+
+ /* Should be reached when hasFinished is set */
+ return(-1);
+ } catch (InterruptedException e) {
+ return(-1);
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ @Override
+ public int read() {
+ byte[] buf = new byte[1];
+ int rv = read(buf, 0, 1);
+
+ if(rv == -1)
+ return(-1);
+ else
+ return(buf[0]);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java
index 4c9661c3..0afdc022 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java
@@ -104,19 +104,27 @@ public class ConnectionsAdapter extends BaseAdapter {
if((now - adapter_conn.first_seen) >= MIN_CONNECTION_DISPLAY_SECONDS)
mItems.remove(adapter_pos);
else
+ /* Too early, let the connection displayed for a while */
adapter_pos++;
adapter_conn = getItem(adapter_pos);
}
if (adapter_conn == null)
+ /* New untracked connection */
mItems.add(eval_conn);
else {
+ /* Existing connection */
if (eval_conn.incr_id == adapter_conn.incr_id) {
/* Update data */
mItems.set(adapter_pos, eval_conn);
} else {
- Log.e(TAG, "Logic error: missing item #" + eval_conn.incr_id +
+ /* This should never happen as it would mean that a connection ID has been
+ * reused. */
+ Log.w(TAG, "Logic error? Missing item #" + eval_conn.incr_id +
" (adapter item: #" + adapter_conn.incr_id + ")");
+
+ /* Try to recover */
+ clear();
}
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/HTTPServer.java b/app/src/main/java/com/emanuelef/remote_capture/HTTPServer.java
new file mode 100644
index 00000000..109ad659
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/HTTPServer.java
@@ -0,0 +1,127 @@
+/*
+ This file is part of RemoteCapture.
+
+ RemoteCapture 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.
+
+ RemoteCapture 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 RemoteCapture. If not, see .
+
+ Copyright 2020 by Emanuele Faranda
+*/
+
+package com.emanuelef.remote_capture;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+
+public class HTTPServer extends NanoHTTPD {
+ private static final String PCAP_MIME = "application/vnd.tcpdump.pcap";
+ private final DateFormat mFmt = new SimpleDateFormat("HH_mm_ss");
+ private boolean firstStart = true;
+ private boolean mAcceptConnections = false;
+ private Context mContext;
+
+ /* NOTE: access to mActiveResponses must be synchronized */
+ private ArrayList mActiveResponses = new ArrayList<>();
+
+ public HTTPServer(Context context, int port) {
+ super(port);
+ mContext = context;
+ }
+
+ private Response redirectToPcap() {
+ String fname = "RemoteCapture_" + mFmt.format(new Date()) + ".pcap";
+ Response r = newFixedLengthResponse(Status.TEMPORARY_REDIRECT, MIME_HTML, "");
+ r.addHeader("Location", "/" + fname);
+ return(r);
+ }
+
+ /* Creates a new Response and add it to the active responses. */
+ private synchronized Response newPcapStream() {
+ /* NOTE: response length is unknown */
+ Response res = newChunkedResponse(Status.OK, PCAP_MIME, new ChunkedInputStream());
+
+ mActiveResponses.add(res);
+
+ return res;
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ firstStart = true;
+ }
+
+ public void startConnections() throws IOException {
+ mAcceptConnections = true;
+
+ if(firstStart) {
+ start();
+ firstStart = false;
+ }
+ }
+
+ /* Marks data end on all the active connections */
+ public synchronized void endConnections() {
+ for(int i=mActiveResponses.size()-1; i >= 0; i--) {
+ Response res = mActiveResponses.get(i);
+
+ if(res.isCloseConnection()) {
+ /* Cleanup closed connections */
+ mActiveResponses.remove(i);
+ continue;
+ }
+
+ ((ChunkedInputStream) res.getData()).stop();
+ }
+
+ mActiveResponses.clear();
+ mAcceptConnections = false;
+ }
+
+ /* Dispatch PCAP data to the active connections */
+ public synchronized void pushData(byte[] data) {
+ for(int i=mActiveResponses.size()-1; i >= 0; i--) {
+ Response res = mActiveResponses.get(i);
+
+ if(res.isCloseConnection()) {
+ /* Cleanup closed connections */
+ mActiveResponses.remove(i);
+ continue;
+ }
+
+ ((ChunkedInputStream) res.getData()).produceData(data);
+ }
+ }
+
+ @Override
+ public Response serve(IHTTPSession session) {
+ if(!mAcceptConnections)
+ return newFixedLengthResponse(Status.FORBIDDEN, MIME_PLAINTEXT,
+ mContext.getString(R.string.capture_not_started));
+
+ if(session.getUri().endsWith("/")) {
+ /* Use a redirect to provide a file name */
+ return redirectToPcap();
+ }
+
+ return newPcapStream();
+ }
+}
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 8ac870a4..2cf6e5d1 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/MainActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/MainActivity.java
@@ -237,16 +237,8 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
if (requestCode == REQUEST_CODE_VPN && resultCode == RESULT_OK) {
Intent intent = new Intent(MainActivity.this, CaptureService.class);
Bundle bundle = new Bundle();
- String dns_server = Utils.getDnsServer(getApplicationContext());
- Log.i("Main", "Using DNS server " + dns_server);
-
- // the configuration for the VPN
- bundle.putString("dns_server", dns_server);
- 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);
- bundle.putBoolean(Prefs.PREF_CAPTURE_UNKNOWN_APP_TRAFFIC, getCaptureUnknownTrafficPref());
intent.putExtra("settings", bundle);
Log.d("Main", "onActivityResult -> start CaptureService");
@@ -399,16 +391,4 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
alert.show();
}
-
- String getCollectorIPPref() {
- return(mPrefs.getString(Prefs.PREF_COLLECTOR_IP_KEY, getString(R.string.default_collector_ip)));
- }
-
- String getCollectorPortPref() {
- return(mPrefs.getString(Prefs.PREF_COLLECTOR_PORT_KEY, getString(R.string.default_collector_port)));
- }
-
- private boolean getCaptureUnknownTrafficPref() {
- return(mPrefs.getBoolean(Prefs.PREF_CAPTURE_UNKNOWN_APP_TRAFFIC, true));
- }
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/Prefs.java b/app/src/main/java/com/emanuelef/remote_capture/Prefs.java
index 9b0bec31..64bfb6f4 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/Prefs.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/Prefs.java
@@ -1,5 +1,7 @@
package com.emanuelef.remote_capture;
+import android.content.SharedPreferences;
+
public class Prefs {
static final String DUMP_HTTP_SERVER = "http_server";
static final String DUMP_UDP_EXPORTER = "udp_exporter";
@@ -8,6 +10,7 @@ public class Prefs {
static final String PREF_UID_FILTER = "uid_filter";
static final String PREF_CAPTURE_UNKNOWN_APP_TRAFFIC = "capture_unknown_app";
static final String PREF_HTTP_SERVER_PORT = "http_server_port";
+ static final String PREF_PCAP_DUMP_MODE = "pcap_dump_mode";
enum DumpMode {
NONE,
@@ -23,4 +26,11 @@ public class Prefs {
else
return(DumpMode.NONE);
}
+
+ /* Prefs with defaults */
+ static String getCollectorIp(SharedPreferences p) { return(p.getString(PREF_COLLECTOR_IP_KEY, "127.0.0.1")); }
+ static int getCollectorPort(SharedPreferences p) { return(Integer.parseInt(p.getString(PREF_COLLECTOR_PORT_KEY, "1234"))); }
+ static boolean getCaptureUnknownAppTraffic(SharedPreferences p) { return(p.getBoolean(PREF_CAPTURE_UNKNOWN_APP_TRAFFIC, true)); }
+ static DumpMode getDumpMode(SharedPreferences p) { return(getDumpMode(p.getString(PREF_PCAP_DUMP_MODE, DUMP_HTTP_SERVER))); }
+ static int getHttpServerPort(SharedPreferences p) { return(Integer.parseInt(p.getString(Prefs.PREF_HTTP_SERVER_PORT, "8080"))); }
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/SettingsActivity.java
index 62631c23..34bbaab1 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/SettingsActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/SettingsActivity.java
@@ -42,7 +42,7 @@ public class SettingsActivity extends AppCompatActivity {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
- mDumpModePref = findPreference("pcap_dump_mode");
+ mDumpModePref = findPreference(Prefs.PREF_PCAP_DUMP_MODE);
mDumpModePref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
diff --git a/app/src/main/java/com/emanuelef/remote_capture/StatusFragment.java b/app/src/main/java/com/emanuelef/remote_capture/StatusFragment.java
index 40626fc0..f4b8f640 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/StatusFragment.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/StatusFragment.java
@@ -1,13 +1,17 @@
package com.emanuelef.remote_capture;
+import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@@ -24,6 +28,7 @@ public class StatusFragment extends Fragment implements AppStateListener {
private TextView mCollectorInfo;
private TextView mCaptureStatus;
private MainActivity mActivity;
+ private SharedPreferences mPrefs;
@Override
public void onAttach(@NonNull Context context) {
@@ -35,6 +40,7 @@ public class StatusFragment extends Fragment implements AppStateListener {
@Override
public void onDestroy() {
mActivity.setStatusFragment(null);
+ mActivity = null;
super.onDestroy();
}
@@ -44,19 +50,40 @@ public class StatusFragment extends Fragment implements AppStateListener {
return inflater.inflate(R.layout.status, container, false);
}
+ @SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
mStartButton = view.findViewById(R.id.button_start);
mCollectorInfo = view.findViewById(R.id.collector_info);
mCaptureStatus = view.findViewById(R.id.status_view);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
- SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
+ // Make URLs clickable
+ mCollectorInfo.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // Add settings icon click
+ mCollectorInfo.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ Drawable mCollectorInfoDrawable = mCollectorInfo.getCompoundDrawables()[2 /* Right */];
+
+ if(event.getAction() == MotionEvent.ACTION_UP) {
+ if(event.getRawX() >= (mCollectorInfo.getRight() - mCollectorInfoDrawable.getBounds().width())) {
+ Intent intent = new Intent(mActivity, SettingsActivity.class);
+ startActivity(intent);
+
+ return true;
+ }
+ }
+ return false;
+ }
+ });
mPrefs.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
- if(mActivity.getState() == MainActivity.AppState.ready)
- setCollectorInfo(mActivity.getCollectorIPPref(), mActivity.getCollectorPortPref());
+ if((mActivity != null) && (mActivity.getState() == MainActivity.AppState.ready))
+ refreshPcapDumpInfo();
}
});
@@ -88,7 +115,7 @@ public class StatusFragment extends Fragment implements AppStateListener {
mStartButton.setEnabled(true);
mCaptureStatus.setText(R.string.ready);
- setCollectorInfo(mActivity.getCollectorIPPref(), mActivity.getCollectorPortPref());
+ refreshPcapDumpInfo();
}
@Override
@@ -102,8 +129,7 @@ public class StatusFragment extends Fragment implements AppStateListener {
mStartButton.setEnabled(true);
mCaptureStatus.setText(Utils.formatBytes(CaptureService.getBytes()));
- setCollectorInfo(CaptureService.getCollectorAddress(),
- Integer.toString(CaptureService.getCollectorPort()));
+ refreshPcapDumpInfo();
}
@Override
@@ -123,8 +149,35 @@ public class StatusFragment extends Fragment implements AppStateListener {
mCaptureStatus.setText(Utils.formatBytes(bytes_sent + bytes_rcvd));
}
- private void setCollectorInfo(String collector_ip, String collector_port) {
- mCollectorInfo.setText(String.format(getResources().getString(R.string.collector_info),
- collector_ip, collector_port));
+ private void refreshPcapDumpInfo() {
+ String info;
+ String modeName;
+
+ Prefs.DumpMode mode = CaptureService.isServiceActive() ? CaptureService.getDumpMode() : Prefs.getDumpMode(mPrefs);
+
+ switch (mode) {
+ case HTTP_SERVER:
+ modeName = getResources().getString(R.string.http_server);
+ info = String.format(getResources().getString(R.string.http_server_status),
+ Utils.getLocalIPAddress(), CaptureService.getHTTPServerPort());
+ break;
+ case UDP_EXPORTER:
+ modeName = getResources().getString(R.string.udp_exporter);
+ info = String.format(getResources().getString(R.string.collector_info),
+ CaptureService.getCollectorAddress(), CaptureService.getCollectorPort());
+ break;
+ default:
+ modeName = getResources().getString(R.string.no_dump);
+ info = "";
+ break;
+ }
+
+ if(!CaptureService.isServiceActive()) {
+ info = getResources().getString(R.string.dump_mode) + ": " + modeName;
+ mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.settins_icon, 0);
+ } else
+ mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+
+ mCollectorInfo.setText(info);
}
}
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 d7457d3c..73f0ab84 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
@@ -12,8 +12,10 @@ import android.os.Build;
import android.util.Log;
import java.net.InetAddress;
+import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collections;
import java.util.List;
public class Utils {
@@ -107,8 +109,38 @@ public class Utils {
return "8.8.8.8";
}
+ public static String getLocalIPAddress() {
+ // https://stackoverflow.com/questions/6064510/how-to-get-ip-address-of-the-device-from-code
+ try {
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface intf : interfaces) {
+ List addrs = Collections.list(intf.getInetAddresses());
+ for (InetAddress addr : addrs) {
+ if (!addr.isLoopbackAddress() && addr.isSiteLocalAddress() /* Exclude public IPs */) {
+ String sAddr = addr.getHostAddress();
+ boolean isIPv4 = sAddr.indexOf(':')<0;
+
+ if(isIPv4)
+ return sAddr;
+ }
+ }
+ }
+ } catch (Exception ignored) { }
+ return "";
+ }
+
public static long now() {
Calendar calendar = Calendar.getInstance();
return(calendar.getTimeInMillis() / 1000);
}
+
+ public static byte[] hexStringToByteArray(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i+1), 16));
+ }
+ return data;
+ }
}
diff --git a/app/src/main/jni/vpnproxy-jni/pcap.c b/app/src/main/jni/vpnproxy-jni/pcap.c
index b4f65584..6865725b 100644
--- a/app/src/main/jni/vpnproxy-jni/pcap.c
+++ b/app/src/main/jni/vpnproxy-jni/pcap.c
@@ -23,6 +23,7 @@
#include
#include
#include
+#include "pcap.h"
/* ******************************************************* */
@@ -30,27 +31,6 @@
#define PCAP_TAG "PCAP_DUMP"
#define SNAPLEN 65535
-typedef uint16_t guint16_t;
-typedef uint32_t guint32_t;
-typedef int32_t gint32_t;
-
-typedef struct pcap_hdr_s {
- guint32_t magic_number;
- guint16_t version_major;
- guint16_t version_minor;
- gint32_t thiszone;
- guint32_t sigfigs;
- guint32_t snaplen;
- guint32_t network;
-} __packed pcap_hdr_s;
-
-typedef struct pcaprec_hdr_s {
- guint32_t ts_sec;
- guint32_t ts_usec;
- guint32_t incl_len;
- guint32_t orig_len;
-} __packed pcaprec_hdr_s;
-
/* ******************************************************* */
static size_t frame_id = 1;
@@ -65,6 +45,30 @@ static void write_pcap(int fd, const struct sockaddr *srv, size_t srv_size, cons
/* ******************************************************* */
+static size_t init_pcap_rec_hdr(struct pcaprec_hdr_s *pcap_rec, int length) {
+ size_t incl_len;
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_REALTIME, &ts))
+ __android_log_print(ANDROID_LOG_ERROR, PCAP_TAG, "clock_gettime error[%d]: %s", errno, strerror(errno));
+
+ incl_len = (length < SNAPLEN ? length : SNAPLEN);
+
+ pcap_rec->ts_sec = (guint32_t) ts.tv_sec;
+ pcap_rec->ts_usec = (guint32_t) (ts.tv_nsec / 1000);
+ pcap_rec->incl_len = (guint32_t) incl_len;
+ pcap_rec->orig_len = (guint32_t) length;
+
+ pcap_rec->ts_sec = (guint32_t) ts.tv_sec;
+ pcap_rec->ts_usec = (guint32_t) (ts.tv_nsec / 1000);
+ pcap_rec->incl_len = (guint32_t) incl_len;
+ pcap_rec->orig_len = (guint32_t) length;
+
+ return(incl_len);
+}
+
+/* ******************************************************* */
+
void write_pcap_hdr(int fd, const struct sockaddr *srv, size_t srv_size) {
struct pcap_hdr_s pcap_hdr;
pcap_hdr.magic_number = 0xa1b2c3d4;
@@ -80,23 +84,28 @@ void write_pcap_hdr(int fd, const struct sockaddr *srv, size_t srv_size) {
/* ******************************************************* */
void write_pcap_rec(int fd, const struct sockaddr *srv, size_t srv_size, const uint8_t *buffer, size_t length) {
- size_t incl_len, tot_len;
- struct timespec ts;
- struct pcaprec_hdr_s *pcap_rec;
+ struct pcaprec_hdr_s *pcap_rec = (struct pcaprec_hdr_s *) pcap_buffer;
- if (clock_gettime(CLOCK_REALTIME, &ts))
- __android_log_print(ANDROID_LOG_ERROR, PCAP_TAG, "clock_gettime error[%d]: %s", errno, strerror(errno));
-
- incl_len = (length < SNAPLEN ? length : SNAPLEN);
- tot_len = sizeof(struct pcaprec_hdr_s) + incl_len;
-
- pcap_rec = (struct pcaprec_hdr_s *) pcap_buffer;
- pcap_rec->ts_sec = (guint32_t) ts.tv_sec;
- pcap_rec->ts_usec = (guint32_t) (ts.tv_nsec / 1000);
- pcap_rec->incl_len = (guint32_t) incl_len;
- pcap_rec->orig_len = (guint32_t) length;
+ size_t incl_len = init_pcap_rec_hdr(pcap_rec, length);
+ size_t tot_len = sizeof(struct pcaprec_hdr_s) + incl_len;
+ // NOTE: use incl_size as the packet may be cut due to the SNAPLEN
memcpy(pcap_buffer + sizeof(struct pcaprec_hdr_s), buffer, incl_len);
write_pcap(fd, srv, srv_size, pcap_rec, tot_len);
}
+
+/* ******************************************************* */
+
+size_t dump_pcap_rec(uint8_t *buffer, const uint8_t *pkt, size_t pkt_len) {
+ struct pcaprec_hdr_s *pcap_rec = (pcaprec_hdr_s*) buffer;
+
+ size_t incl_len = init_pcap_rec_hdr(pcap_rec, pkt_len);
+ size_t tot_len = sizeof(struct pcaprec_hdr_s) + incl_len;
+
+ // NOTE: use incl_size as the packet may be cut due to the SNAPLEN
+ // Assumption: there is enough available space in buffer
+ memcpy(buffer + sizeof(struct pcaprec_hdr_s), pkt, incl_len);
+
+ return(tot_len);
+}
\ No newline at end of file
diff --git a/app/src/main/jni/vpnproxy-jni/pcap.h b/app/src/main/jni/vpnproxy-jni/pcap.h
index 048a5452..e8f58b6e 100644
--- a/app/src/main/jni/vpnproxy-jni/pcap.h
+++ b/app/src/main/jni/vpnproxy-jni/pcap.h
@@ -17,5 +17,32 @@
Copyright 2019 by Emanuele Faranda
*/
+#ifndef __MY_PCAP_H__
+#define __MY_PCAP_H__
+
+typedef uint16_t guint16_t;
+typedef uint32_t guint32_t;
+typedef int32_t gint32_t;
+
+typedef struct pcap_hdr_s {
+ guint32_t magic_number;
+ guint16_t version_major;
+ guint16_t version_minor;
+ gint32_t thiszone;
+ guint32_t sigfigs;
+ guint32_t snaplen;
+ guint32_t network;
+} __packed pcap_hdr_s;
+
+typedef struct pcaprec_hdr_s {
+ guint32_t ts_sec;
+ guint32_t ts_usec;
+ guint32_t incl_len;
+ guint32_t orig_len;
+} __packed pcaprec_hdr_s;
+
void write_pcap_hdr(int fd, const struct sockaddr *srv, size_t srv_size);
void write_pcap_rec(int fd, const struct sockaddr *srv, size_t srv_size, const uint8_t *buffer, size_t length);
+size_t dump_pcap_rec(uint8_t *buffer, const uint8_t *pkt, size_t pkt_len);
+
+#endif // __MY_PCAP_H__
\ 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 4d1a0d74..cc429685 100644
--- a/app/src/main/jni/vpnproxy-jni/vpnproxy.c
+++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.c
@@ -27,7 +27,9 @@
#define VPN_TAG "VPNProxy"
#define CAPTURE_STATS_UPDATE_FREQUENCY_MS 300
#define CONNECTION_DUMP_UPDATE_FREQUENCY_MS 3000
+#define MAX_JAVA_DUMP_DELAY_MS 1000
#define MAX_DPI_PACKETS 12
+#define JAVA_PCAP_BUFFER_SIZE (1*1024*1204)
/* ******************************************************* */
@@ -299,6 +301,27 @@ static void process_ndpi_packet(conn_data_t *data, vpnproxy_data_t *proxy, const
/* ******************************************************* */
+static void javaPcapDump(zdtun_t *tun, vpnproxy_data_t *proxy) {
+ JNIEnv *env = proxy->env;
+ jclass vpn_service_cls = (*env)->GetObjectClass(env, proxy->vpn_service);
+
+ jmethodID midMethod = (*env)->GetMethodID(env, vpn_service_cls, "dumpPcapData", "([B)V");
+ if(!midMethod) {
+ __android_log_print(ANDROID_LOG_FATAL, VPN_TAG, "GetMethodID(dumpPcapData) failed");
+ return;
+ }
+
+ jbyteArray barray = (*env)->NewByteArray(env, proxy->java_dump.buffer_idx);
+ (*env)->SetByteArrayRegion(env, barray, 0, proxy->java_dump.buffer_idx, proxy->java_dump.buffer);
+
+ (*env)->CallVoidMethod(env, proxy->vpn_service, midMethod, barray);
+
+ proxy->java_dump.buffer_idx = 0;
+ proxy->java_dump.last_dump_ms = proxy->now_ms;
+}
+
+/* ******************************************************* */
+
static void account_packet(zdtun_t *tun, const char *packet, ssize_t size, uint8_t from_tap, const zdtun_conn_t *conn_info) {
struct sockaddr_in servaddr = {0};
conn_data_t *data = (conn_data_t*)conn_info->user_data;
@@ -336,9 +359,9 @@ static void account_packet(zdtun_t *tun, const char *packet, ssize_t size, uint8
if(data->ndpi_flow)
process_ndpi_packet(data, proxy, packet, size, from_tap);
- if(((proxy->pcap_dump.uid_filter != -1) && (proxy->pcap_dump.uid_filter != uid))
- && (!is_unknown_app || !proxy->pcap_dump.capture_unknown_app_traffic)) {
- //__android_log_print(ANDROID_LOG_DEBUG, VPN_TAG, "Discarding connection: UID=%d [filter=%d]", uid, proxy->pcap_dump.uid_filter);
+ if(((proxy->uid_filter != -1) && (proxy->uid_filter != uid))
+ && (!is_unknown_app || !proxy->capture_unknown_app_traffic)) {
+ //__android_log_print(ANDROID_LOG_DEBUG, VPN_TAG, "Discarding connection: UID=%d [filter=%d]", uid, proxy->uid_filter);
return;
}
@@ -353,21 +376,30 @@ static void account_packet(zdtun_t *tun, const char *packet, ssize_t size, uint8
/* New stats to notify */
proxy->capture_stats.new_stats = true;
- if(dumper_socket <= 0)
- return;
+ if(proxy->java_dump.buffer) {
+ int rem_size = JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx;
-#if 1
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = proxy->pcap_dump.collector_port;
- servaddr.sin_addr.s_addr = proxy->pcap_dump.collector_addr;
+ if((size + sizeof(pcaprec_hdr_s)) > rem_size) {
+ // Flush the buffer
+ javaPcapDump(tun, proxy);
+ }
- if(send_header) {
- write_pcap_hdr(dumper_socket, (struct sockaddr *) &servaddr, sizeof(servaddr));
- send_header = false;
+ proxy->java_dump.buffer_idx += dump_pcap_rec(proxy->java_dump.buffer + proxy->java_dump.buffer_idx, packet, size);
}
- write_pcap_rec(dumper_socket, (struct sockaddr *)&servaddr, sizeof(servaddr), (u_int8_t*)packet, size);
-#endif
+ 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);
+ }
}
/* ******************************************************* */
@@ -816,13 +848,16 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
.vpn_dns = getIPv4Pref(&proxy, "getVpnDns"),
.public_dns = getIPv4Pref(&proxy, "getPublicDns"),
.incr_id = 0,
+ .uid_filter = getIntPref(&proxy, "getPcapUidFilter"),
+ .capture_unknown_app_traffic = getIntPref(&proxy, "getCaptureUnknownTraffic"),
+ .java_dump = {
+ .enabled = getIntPref(&proxy, "dumpPcapToJava"),
+ },
.pcap_dump = {
.collector_addr = getIPv4Pref(&proxy, "getPcapCollectorAddress"),
.collector_port = htons(getIntPref(&proxy, "getPcapCollectorPort")),
- .uid_filter = getIntPref(&proxy, "getPcapUidFilter"),
.tcp_socket = false,
- .capture_unknown_app_traffic = getIntPref(&proxy, "getCaptureUnknownTraffic"),
- .enabled = true,
+ .enabled = getIntPref(&proxy, "dumpPcapToUdp"),
},
};
zdtun_callbacks_t callbacks = {
@@ -873,6 +908,17 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
running = false;
}
+ if(proxy.java_dump.enabled) {
+ proxy.java_dump.buffer = malloc(JAVA_PCAP_BUFFER_SIZE);
+ proxy.java_dump.buffer_idx = 0;
+
+ if(!proxy.java_dump.buffer) {
+ __android_log_print(ANDROID_LOG_ERROR, VPN_TAG, "malloc(java_dump.buffer) failed with code %d/%s",
+ errno, strerror(errno));
+ running = false;
+ }
+ }
+
while(running) {
int max_fd;
fd_set fdset;
@@ -894,6 +940,7 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
gettimeofday(&now_tv, NULL);
now_ms = now_tv.tv_sec * 1000 + now_tv.tv_usec / 1000;
+ proxy.now_ms = now_ms;
if(FD_ISSET(tapfd, &fdset)) {
/* Packet from VPN */
@@ -920,11 +967,12 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
sendCaptureStats(&proxy);
proxy.capture_stats.new_stats = false;
proxy.capture_stats.last_update_ms = now_ms;
- }
-
- if((now_ms - last_connections_dump) >= CONNECTION_DUMP_UPDATE_FREQUENCY_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(tun, &proxy);
}
}
@@ -948,6 +996,11 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
dumper_socket = -1;
}
+ if(proxy.java_dump.buffer) {
+ free(proxy.java_dump.buffer);
+ proxy.java_dump.buffer = NULL;
+ }
+
notifyServiceStatus(&proxy, "stopped");
return(0);
}
diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.h b/app/src/main/jni/vpnproxy-jni/vpnproxy.h
index 9e3d84f4..f60ed653 100644
--- a/app/src/main/jni/vpnproxy-jni/vpnproxy.h
+++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.h
@@ -49,16 +49,24 @@ typedef struct vpnproxy_data {
zdtun_conn_t *notif_pending;
u_int32_t cur_notif_pending;
u_int32_t notif_pending_size;
+ int uid_filter;
+ bool capture_unknown_app_traffic;
+ uint64_t now_ms;
struct {
u_int32_t collector_addr;
u_int16_t collector_port;
- int uid_filter;
bool tcp_socket;
- bool capture_unknown_app_traffic;
bool enabled;
} pcap_dump;
+ struct {
+ bool enabled;
+ u_char *buffer;
+ int buffer_idx;
+ u_int64_t last_dump_ms;
+ } java_dump;
+
capture_stats_t capture_stats;
} vpnproxy_data_t;
diff --git a/app/src/main/res/drawable-anydpi/settins_icon.xml b/app/src/main/res/drawable-anydpi/settins_icon.xml
new file mode 100644
index 00000000..61e00515
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi/settins_icon.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-hdpi/settins_icon.png b/app/src/main/res/drawable-hdpi/settins_icon.png
new file mode 100644
index 00000000..a833fecc
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/settins_icon.png differ
diff --git a/app/src/main/res/drawable-mdpi/settins_icon.png b/app/src/main/res/drawable-mdpi/settins_icon.png
new file mode 100644
index 00000000..2c7f6835
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/settins_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/settins_icon.png b/app/src/main/res/drawable-xhdpi/settins_icon.png
new file mode 100644
index 00000000..bc0a8b34
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/settins_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/settins_icon.png b/app/src/main/res/drawable-xxhdpi/settins_icon.png
new file mode 100644
index 00000000..a11cf0cd
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/settins_icon.png differ
diff --git a/app/src/main/res/layout/status.xml b/app/src/main/res/layout/status.xml
index 0a031671..7ac83f8f 100644
--- a/app/src/main/res/layout/status.xml
+++ b/app/src/main/res/layout/status.xml
@@ -23,7 +23,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="92dp"
+ android:gravity="center"
android:text="TextView"
+ android:autoLink="web"
+ android:drawablePadding="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 694b54b9..2e398af9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,12 +4,14 @@
Stop
Settings
No Filter
- UDP Collector: %1$s:%2$s
+ UDP Collector: %1$s:%2$d
+ HTTP Server: http://%1$s:%2$d
%1$s:%2$d
%1$s down — %2$s up
%1$s (%2$s)
Query
Host
+ Capture not running!
127.0.0.1
@@ -45,9 +47,10 @@
None
PCAP will not be dumped
Start an HTTP server for the PCAP download
- Sends the PCAP to an UDP receiver
+ Sends the PCAP to a remote UDP receiver
HTTP Server Port
Receiver IP Address
Receiver Port
+ Dump Mode
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 4f908d15..d1f0c577 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -14,13 +14,12 @@
~ limitations under the License.
-->
-
+