Implement ability to save the PCAP file in the device

Closes #24
This commit is contained in:
emanuele-f 2021-03-01 17:57:34 +01:00
parent 3e08121edc
commit 7e7d469666
11 changed files with 227 additions and 33 deletions

View File

@ -25,6 +25,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.net.VpnService;
import android.os.Build;
import android.os.Bundle;
@ -41,7 +42,9 @@ import com.emanuelef.remote_capture.model.ConnectionDescriptor;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.model.VPNStats;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
public class CaptureService extends VpnService implements Runnable {
@ -64,7 +67,10 @@ public class CaptureService extends VpnService implements Runnable {
private static CaptureService INSTANCE;
private String app_filter;
private HTTPServer mHttpServer;
private OutputStream mOutputStream;
private ConnectionsRegister conn_reg;
private Uri mPcapUri;
private boolean mFirstStreamWrite;
/* The maximum connections to log into the ConnectionsRegister. Older connections are dropped.
* Max Estimated max memory usage: less than 2 MB. */
@ -140,6 +146,11 @@ public class CaptureService extends VpnService implements Runnable {
conn_reg = new ConnectionsRegister(CONNECTIONS_LOG_SIZE);
if(dump_mode != Prefs.DumpMode.HTTP_SERVER)
mHttpServer = null;
mOutputStream = null;
mPcapUri = null;
if(dump_mode == Prefs.DumpMode.HTTP_SERVER) {
if (mHttpServer == null)
mHttpServer = new HTTPServer(app_ctx, http_server_port);
@ -150,8 +161,25 @@ public class CaptureService extends VpnService implements Runnable {
Log.e(CaptureService.TAG, "Could not start the HTTP server");
e.printStackTrace();
}
} else
mHttpServer = null;
} else if(dump_mode == Prefs.DumpMode.PCAP_FILE) {
String path = settings.getString(Prefs.PREF_PCAP_URI);
if(path != null) {
mPcapUri = Uri.parse(path);
try {
mOutputStream = getContentResolver().openOutputStream(mPcapUri);
mFirstStreamWrite = true;
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
if(mOutputStream == null) {
Utils.showToast(this, R.string.cannot_write_pcap_file);
return super.onStartCommand(intent, flags, startId);
}
}
Log.i(TAG, "Using DNS server " + public_dns);
@ -244,6 +272,16 @@ public class CaptureService extends VpnService implements Runnable {
if(mHttpServer != null)
mHttpServer.endConnections();
// NOTE: do not destroy the mHttpServer, let it terminate the active connections
if(mOutputStream != null) {
try {
mOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
mPcapUri = null;
}
/* Check if the VPN service was launched */
@ -256,6 +294,10 @@ public class CaptureService extends VpnService implements Runnable {
return((INSTANCE != null) ? INSTANCE.app_filter : null);
}
public static Uri getPcapUri() {
return ((INSTANCE != null) ? INSTANCE.mPcapUri : null);
}
public static long getBytes() {
return((INSTANCE != null) ? INSTANCE.last_bytes : 0);
}
@ -334,7 +376,7 @@ public class CaptureService extends VpnService implements Runnable {
// returns 1 if dumpPcapData should be called
public int dumpPcapToJava() {
return((mHttpServer != null) ? 1 : 0);
return(((dump_mode == Prefs.DumpMode.HTTP_SERVER) || (dump_mode == Prefs.DumpMode.PCAP_FILE)) ? 1 : 0);
}
public int dumpPcapToUdp() {
@ -410,6 +452,20 @@ public class CaptureService extends VpnService implements Runnable {
public void dumpPcapData(byte[] data) {
if(mHttpServer != null)
mHttpServer.pushData(data);
else if(mOutputStream != null) {
try {
if(mFirstStreamWrite) {
mOutputStream.write(Utils.hexStringToByteArray(Utils.PCAP_HEADER));
mFirstStreamWrite = false;
}
mOutputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
reportError(e.getLocalizedMessage());
stopPacketLoop();
}
}
}
public void reportError(String msg) {

View File

@ -30,7 +30,7 @@ import java.util.concurrent.locks.ReentrantLock;
single bytes[] in order to avoid excessive data copies.
*/
class ChunkedInputStream extends InputStream {
private static final byte[] pcapHeader = Utils.hexStringToByteArray("d4c3b2a1020004000000000000000000ffff000065000000");
private static final byte[] pcapHeader = Utils.hexStringToByteArray(Utils.PCAP_HEADER);
final Lock mLock = new ReentrantLock();
final Condition newData = mLock.newCondition();
ArrayList<byte[]> mChunks = new ArrayList<byte[]>();

View File

@ -20,20 +20,15 @@
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 final Context mContext;
@ -47,7 +42,7 @@ public class HTTPServer extends NanoHTTPD {
}
private Response redirectToPcap() {
String fname = "PCAPdroid_" + mFmt.format(new Date()) + ".pcap";
String fname = Utils.getUniquePcapFileName(mContext);
Response r = newFixedLengthResponse(Status.TEMPORARY_REDIRECT, MIME_HTML, "");
r.addHeader("Location", "/" + fname);
return(r);

View File

@ -46,12 +46,17 @@ import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.nio.ByteOrder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class Utils {
public static final String PCAP_HEADER = "d4c3b2a1020004000000000000000000ffff000065000000";
public static String formatBytes(long bytes) {
long divisor;
String suffix;
@ -269,4 +274,10 @@ public class Utils {
alert.show();
}
public static String getUniquePcapFileName(Context context) {
Locale locale = context.getResources().getConfiguration().locale;
final DateFormat fmt = new SimpleDateFormat("dd_MMM_HH_mm_ss", locale);
return "PCAPdroid_" + fmt.format(new Date()) + ".pcap";
}
}

View File

@ -26,6 +26,7 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.VpnService;
@ -41,6 +42,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@ -59,6 +62,7 @@ import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.google.android.material.navigation.NavigationView;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -68,22 +72,25 @@ import java.util.Objects;
import cat.ereza.customactivityoncrash.config.CaocConfig;
public class MainActivity extends AppCompatActivity implements AppsLoadListener, NavigationView.OnNavigationItemSelectedListener {
SharedPreferences mPrefs;
Menu mMenu;
MenuItem mMenuItemStartBtn;
MenuItem mMenuItemAppSel;
MenuItem mMenuSettings;
Drawable mFilterIcon;
String mFilterApp;
boolean mOpenAppsWhenDone;
List<AppDescriptor> mInstalledApps;
AppState mState;
AppStateListener mListener;
AppDescriptor mNoFilterApp;
private SharedPreferences mPrefs;
private Menu mMenu;
private MenuItem mMenuItemStartBtn;
private MenuItem mMenuItemAppSel;
private MenuItem mMenuSettings;
private Drawable mFilterIcon;
private String mFilterApp;
private boolean mOpenAppsWhenDone;
private List<AppDescriptor> mInstalledApps;
private AppState mState;
private AppStateListener mListener;
private AppDescriptor mNoFilterApp;
private Uri mPcapUri;
private BroadcastReceiver mReceiver;
private static final String TAG = "Main";
private static final int REQUEST_CODE_VPN = 2;
private static final int REQUEST_CODE_PCAP_FILE = 3;
public static final String TELEGRAM_GROUP_NAME = "PCAPdroid";
public static final String GITHUB_PROJECT_URL = "https://github.com/emanuele-f/PCAPdroid";
@ -97,6 +104,7 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
mNoFilterApp = new AppDescriptor("", icon, this.getResources().getString(R.string.no_filter), -1, false, true);
mFilterApp = CaptureService.getAppFilter();
mPcapUri = CaptureService.getPcapUri();
if((mFilterApp == null) && (savedInstanceState != null)) {
// Possibly get the temporary filter
@ -117,10 +125,8 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
.setAppsLoadListener(this)
.loadAllApps();
LocalBroadcastManager bcast_man = LocalBroadcastManager.getInstance(this);
/* Register for service status */
bcast_man.registerReceiver(new BroadcastReceiver() {
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String status = intent.getStringExtra(CaptureService.SERVICE_STATUS_KEY);
@ -133,11 +139,26 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
if (CaptureService.isServiceActive())
CaptureService.stopService();
if((mPcapUri != null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
showPcapActionDialog(mPcapUri);
mPcapUri = null;
}
appStateReady();
}
}
}
}, new IntentFilter(CaptureService.ACTION_SERVICE_STATUS));
};
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(CaptureService.ACTION_SERVICE_STATUS));
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mReceiver != null)
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
}
@Override
@ -383,6 +404,9 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
Intent intent = new Intent(MainActivity.this, CaptureService.class);
Bundle bundle = new Bundle();
if((mPcapUri != null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE))
bundle.putString(Prefs.PREF_PCAP_URI, mPcapUri.toString());
bundle.putString(Prefs.PREF_APP_FILTER, mFilterApp);
intent.putExtra("settings", bundle);
@ -393,6 +417,14 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
Log.w(TAG, "VPN request failed");
appStateReady();
}
} else if(requestCode == REQUEST_CODE_PCAP_FILE) {
if(resultCode == RESULT_OK) {
mPcapUri = data.getData();
Log.d(TAG, "PCAP to write: " + mPcapUri.toString());
toggleService();
} else
mPcapUri = null;
}
}
@ -421,6 +453,11 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
appStateStopping();
CaptureService.stopService();
} else {
if((mPcapUri == null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
openFileSelector();
return;
}
if(Utils.hasVPNRunning(this)) {
new AlertDialog.Builder(this)
.setMessage(R.string.existing_vpn_confirm)
@ -432,6 +469,77 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
}
}
public void openFileSelector() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/cap");
intent.putExtra(Intent.EXTRA_TITLE, Utils.getUniquePcapFileName(this));
startActivityForResult(intent, REQUEST_CODE_PCAP_FILE);
}
public void showPcapActionDialog(Uri pcapUri) {
Cursor cursor;
try {
cursor = getContentResolver().query(pcapUri, null, null, null, null);
} catch (Exception e) {
return;
}
if((cursor == null) || !cursor.moveToFirst())
return;
// If file is empty, delete it
long file_size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
String fname = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
cursor.close();
if(file_size == 0) {
Log.d(TAG, "PCAP file is empty, deleting");
try {
DocumentsContract.deleteDocument(getContentResolver(), pcapUri);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return;
}
String message = String.format(getResources().getString(R.string.pcap_file_action), fname, Utils.formatBytes(file_size));
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setMessage(message);
builder.setPositiveButton(R.string.share, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.setType("application/cap");
sendIntent.putExtra(Intent.EXTRA_STREAM, pcapUri);
startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.share)));
});
builder.setNegativeButton(R.string.delete, (dialog, which) -> {
Log.d(TAG, "Deleting PCAP file" + pcapUri.getPath());
boolean deleted = false;
try {
deleted = DocumentsContract.deleteDocument(getContentResolver(), pcapUri);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if(!deleted)
Utils.showToast(MainActivity.this, R.string.delete_error);
dialog.cancel();
});
builder.setNeutralButton(R.string.ok, (dialog, which) -> {
dialog.cancel();
});
builder.create().show();
}
public AppState getState() {
return(mState);
}

View File

@ -146,6 +146,9 @@ public class SettingsActivity extends AppCompatActivity {
case HTTP_SERVER:
summary_id = R.string.http_server_info;
break;
case PCAP_FILE:
summary_id = R.string.pcap_file_info;
break;
case UDP_EXPORTER:
summary_id = R.string.udp_exporter_info;
break;

View File

@ -37,6 +37,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
@ -51,8 +52,6 @@ import com.emanuelef.remote_capture.activities.MainActivity;
import com.emanuelef.remote_capture.activities.StatsActivity;
import com.emanuelef.remote_capture.interfaces.AppStateListener;
import org.w3c.dom.Text;
public class StatusFragment extends Fragment implements AppStateListener {
private TextView mCollectorInfo;
private TextView mCaptureStatus;
@ -120,10 +119,10 @@ public class StatusFragment extends Fragment implements AppStateListener {
}
private void setupInspectorLinK() {
int color = getResources().getColor(android.R.color.tab_indicator_text);
int color = ContextCompat.getColor(mActivity, android.R.color.tab_indicator_text);
mInspectorLink.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0);
Drawable drawables[] = mInspectorLink.getCompoundDrawables();
Drawable []drawables = mInspectorLink.getCompoundDrawables();
drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN);
mInspectorLink.setOnClickListener(v -> {
@ -156,6 +155,10 @@ public class StatusFragment extends Fragment implements AppStateListener {
info = String.format(getResources().getString(R.string.http_server_status),
Utils.getLocalIPAddress(mActivity), CaptureService.getHTTPServerPort());
break;
case PCAP_FILE:
modeName = getResources().getString(R.string.pcap_file);
info = "";
break;
case UDP_EXPORTER:
modeName = getResources().getString(R.string.udp_exporter);
info = String.format(getResources().getString(R.string.collector_info),

View File

@ -24,6 +24,7 @@ import android.content.SharedPreferences;
public class Prefs {
public static final String DUMP_HTTP_SERVER = "http_server";
public static final String DUMP_UDP_EXPORTER = "udp_exporter";
public static final String DUMP_PCAP_FILE = "pcap_file";
public static final String PREF_COLLECTOR_IP_KEY = "collector_ip_address";
public static final String PREF_COLLECTOR_PORT_KEY = "collector_port";
public static final String PREF_TLS_PROXY_IP_KEY = "tls_proxy_ip_address";
@ -32,16 +33,20 @@ public class Prefs {
public static final String PREF_APP_FILTER = "app_filter";
public static final String PREF_HTTP_SERVER_PORT = "http_server_port";
public static final String PREF_PCAP_DUMP_MODE = "pcap_dump_mode";
public static final String PREF_PCAP_URI = "pcap_path";
public enum DumpMode {
NONE,
HTTP_SERVER,
PCAP_FILE,
UDP_EXPORTER
}
public static DumpMode getDumpMode(String pref) {
if(pref.equals(DUMP_HTTP_SERVER))
return(DumpMode.HTTP_SERVER);
else if(pref.equals(DUMP_PCAP_FILE))
return(DumpMode.PCAP_FILE);
else if(pref.equals(DUMP_UDP_EXPORTER))
return(DumpMode.UDP_EXPORTER);
else

View File

@ -371,7 +371,7 @@ static void process_ndpi_packet(conn_data_t *data, vpnproxy_data_t *proxy, const
/* ******************************************************* */
static void javaPcapDump(zdtun_t *tun, vpnproxy_data_t *proxy) {
static void javaPcapDump(vpnproxy_data_t *proxy) {
JNIEnv *env = proxy->env;
log_android(ANDROID_LOG_DEBUG, "Exporting a %d B PCAP buffer", proxy->java_dump.buffer_idx);
@ -470,7 +470,7 @@ static void account_packet(zdtun_t *tun, const char *packet, int size, uint8_t f
if((JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx) <= tot_size) {
// Flush the buffer
javaPcapDump(tun, proxy);
javaPcapDump(proxy);
}
if((JAVA_PCAP_BUFFER_SIZE - proxy->java_dump.buffer_idx) <= tot_size)
@ -1108,7 +1108,7 @@ housekeeping:
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);
javaPcapDump(&proxy);
} else if((now_ms >= next_purge_ms) || dump_vpn_stats_now) {
dump_vpn_stats_now = false;
zdtun_statistics_t stats;
@ -1135,6 +1135,9 @@ housekeeping:
}
if(proxy.java_dump.buffer) {
if(proxy.java_dump.buffer_idx > 0)
javaPcapDump(&proxy);
free(proxy.java_dump.buffer);
proxy.java_dump.buffer = NULL;
}

View File

@ -3,12 +3,14 @@
<string-array name="pcap_dump_modes">
<item>none</item>
<item>http_server</item>
<item>pcap_file</item>
<item>udp_exporter</item>
</string-array>
<string-array name="pcap_dump_modes_labels">
<item>@string/no_dump</item>
<item>@string/http_server</item>
<item>@string/pcap_file</item>
<item>@string/udp_exporter</item>
</string-array>
</resources>

View File

@ -89,5 +89,13 @@
<string name="navigation_drawer_open">Drawer Open</string>
<string name="navigation_drawer_close">Drawer Close</string>
<string name="inspector">Inspector</string>
<string name="pcap_file">PCAP File</string>
<string name="pcap_file_info">Create a PCAP file into the device storage</string>
<string name="cannot_write_pcap_file">Cannot write PCAP file</string>
<string name="share">Share</string>
<string name="delete">Delete</string>
<string name="ok">Ok</string>
<string name="pcap_file_action">Traffic saved to file %1$s (%2$s).</string>
<string name="delete_error">Could not delete file</string>
</resources>