Implement embedded HTTP server to provide an easy PCAP download option

It's now possible to choose the PCAP dump mode: none (no external PCAP dump), HTTP server, UDP exporter.
The HTTP server mode is a convenient way to download a PCAP without any client side configuration.
This also makes it possible to capture the PCAP data on a Windows pc, where the UDP collection mode is
not normally applicable.

Closes #13
This commit is contained in:
emanuele-f 2020-01-06 15:45:43 +01:00
parent 930bfaa218
commit 14435311a5
22 changed files with 612 additions and 99 deletions

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
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<byte[]> mChunks = new ArrayList<byte[]>();
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]);
}
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
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<Response> 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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface intf : interfaces) {
List<InetAddress> 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;
}
}

View File

@ -23,6 +23,7 @@
#include <sys/socket.h>
#include <android/log.h>
#include <errno.h>
#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);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="#FF000000"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

View File

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

View File

@ -4,12 +4,14 @@
<string name="stop_button">Stop</string>
<string name="title_activity_settings">Settings</string>
<string name="no_filter">No Filter</string>
<string name="collector_info">UDP Collector: %1$s:%2$s</string>
<string name="collector_info">UDP Collector: %1$s:%2$d</string>
<string name="http_server_status">HTTP Server: http://%1$s:%2$d</string>
<string name="ip_and_port">%1$s:%2$d</string>
<string name="up_and_down">%1$s down — %2$s up</string>
<string name="app_and_proto">%1$s (%2$s)</string>
<string name="query">Query</string>
<string name="host">Host</string>
<string name="capture_not_started">Capture not running!</string>
<!-- Defaults -->
<string name="default_collector_ip">127.0.0.1</string>
@ -45,9 +47,10 @@
<string name="no_dump">None</string>
<string name="no_dump_info">PCAP will not be dumped</string>
<string name="http_server_info">Start an HTTP server for the PCAP download</string>
<string name="udp_exporter_info">Sends the PCAP to an UDP receiver</string>
<string name="udp_exporter_info">Sends the PCAP to a remote UDP receiver</string>
<string name="http_server_port">HTTP Server Port</string>
<string name="receiver_ip_address">Receiver IP Address</string>
<string name="receiver_port">Receiver Port</string>
<string name="dump_mode">Dump Mode</string>
</resources>

View File

@ -14,13 +14,12 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="PCAP Dump" app:iconSpaceReserved="false">
<ListPreference
app:key="pcap_dump_mode"
app:title="Dump Mode"
app:title="@string/dump_mode"
app:iconSpaceReserved="false"
app:summary="@string/no_dump_info"
app:defaultValue="http_server"