From 14435311a56dfc4a250412d218896f3676c54750 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Mon, 6 Jan 2020 15:45:43 +0100 Subject: [PATCH] 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 --- app/build.gradle | 1 + .../remote_capture/CaptureService.java | 66 ++++++++- .../remote_capture/ChunkedInputStream.java | 135 ++++++++++++++++++ .../remote_capture/ConnectionsAdapter.java | 10 +- .../emanuelef/remote_capture/HTTPServer.java | 127 ++++++++++++++++ .../remote_capture/MainActivity.java | 20 --- .../com/emanuelef/remote_capture/Prefs.java | 10 ++ .../remote_capture/SettingsActivity.java | 2 +- .../remote_capture/StatusFragment.java | 71 +++++++-- .../com/emanuelef/remote_capture/Utils.java | 32 +++++ app/src/main/jni/vpnproxy-jni/pcap.c | 79 +++++----- app/src/main/jni/vpnproxy-jni/pcap.h | 27 ++++ app/src/main/jni/vpnproxy-jni/vpnproxy.c | 93 +++++++++--- app/src/main/jni/vpnproxy-jni/vpnproxy.h | 12 +- .../main/res/drawable-anydpi/settins_icon.xml | 11 ++ .../main/res/drawable-hdpi/settins_icon.png | Bin 0 -> 534 bytes .../main/res/drawable-mdpi/settins_icon.png | Bin 0 -> 393 bytes .../main/res/drawable-xhdpi/settins_icon.png | Bin 0 -> 680 bytes .../main/res/drawable-xxhdpi/settins_icon.png | Bin 0 -> 1005 bytes app/src/main/res/layout/status.xml | 3 + app/src/main/res/values/strings.xml | 7 +- app/src/main/res/xml/root_preferences.xml | 5 +- 22 files changed, 612 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/com/emanuelef/remote_capture/ChunkedInputStream.java create mode 100644 app/src/main/java/com/emanuelef/remote_capture/HTTPServer.java create mode 100644 app/src/main/res/drawable-anydpi/settins_icon.xml create mode 100644 app/src/main/res/drawable-hdpi/settins_icon.png create mode 100644 app/src/main/res/drawable-mdpi/settins_icon.png create mode 100644 app/src/main/res/drawable-xhdpi/settins_icon.png create mode 100644 app/src/main/res/drawable-xxhdpi/settins_icon.png 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 0000000000000000000000000000000000000000..a833feccac3bd8f50eaf423dc29e8e2fa93d4349 GIT binary patch literal 534 zcmV+x0_pvUP)Z)JX6dqtX&VTe3)F4WUZ=F@AdQfd~>ZGk5Y_)Paj(!aZ|x z-(#b3TK{DYhr_G!c>J6ucvm^0e!oAZ!++DH*XzwHCo~$3Rw6<~)~lQlSs@S+8Vm;C zRr6_*g?1uBkiz50G|g)|bW6bRg0|+Q2aaBZ;0v+fk_7G!Dos@piJy=(4+itVSz`Ug3mraOV`V~ddxss6MI4_#O zKDl5H!Lz12yPWzGGQk4NvTl?5*uy&x28Bk%vCxw$rE#EP7#AW8yi<`PYyME;bXajj z0e3?R8xdHVWzsAq`JfQxLo4lF*S!=3x6j28=>t1caU|ts%Gb>``rSN*&@ywi&P#~g z{#qEJB3wq~KMQdg0#26xoS+7VB*QjH9BvQmASLsrJ=K`C0%L~~!|7vDp28SU18XKm z$nkqOOY1-Y>$VQGDJ3B^JT$(tn=69*Fk^8lb-Qdr7;$t*RIjW74Z~lSiLr%DNRF@= zOXKlWWr4+^UnNG^Eym+AE{W2V)*4ZZSh(s)E{Z`NH&;($p;a9h#hK*hIz9c_a9VY( YKL7yc1PE2*c>n+a07*qoM6N<$f<(6KW&i*H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2c7f68354f6ed3ea17962855ebdf7c5d8e884c1c GIT binary patch literal 393 zcmV;40e1e0P)4qH5RJt{X~77E;-UYbCEuH+heA#oP#RDKL4QnsdgoCC!*q5vF$bXwb4b{{H}huZ zB?$h-Ns?rqrs*dSyc><6D2fkb%!x;qWzlE^d7fK^;A;>VeY~JB1RCNwilSGl;9DWs zmSy>JP9P-N3cCB|1#jE-fxsFDvXQ`o|kix0>S`cgq_)$-?JFxu&`!j nW9?}F1I-q^THoC-^RCYi7=z=~hW>IC00000NkvXXu0mjfte~+m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bc0a8b34bf326458d66aebce8de7bd5bac03e2e9 GIT binary patch literal 680 zcmV;Z0$2TsP)cygM)IFt6bLBdc8jKegCrC?Otyqyu&xn8W4tI(&=5GbW;0QpKJgCb=0dcAXN z*e2!1Lhu)n9|Xb4=Dp{6R}>xxs>*MO{AHMi5_p0)WC&UFv>Z-kdEO#?iEv22v8v-{ z7_kHo!E3n%YA-D1#jeau7330(ilXSk*Z^YC-IDTa10;F_eG|3TEGsrQu97ukm0kTp~5W}kE zcxNVEmgwGONwR9Kn4@F7m4!G)nKH`a3z^OvAcLk=1B!rD#LGb zcuv?o0YX^LE*XbW6ln^%WsXp8g3St&M^TB4ct2@o`2dj9flXLcDFNk8N(q!VI>+0v zSbTC{li~VEAv8|QOTO}&wOr-;KkFBWhG4UM3-2cY O0000Q5y7@A;!<9Mc&5DgeijntNkwm*iS?tJlTkkH;fX7`q5C)uQF z+1k<7rHtpKOr??0u7y;2#yUhkRr0L5{< zuL=;2gKYrlPy-vJNNMs}PEN?nEOTH`S<~#*j_`(-};LOR2 z9&z5lJHCLkG!D|1W$~yX;Lmyw4Nd(?InI18uPn|wuSvzUE(AJ!F(g>LX338)h9#svAFh6H!&NmecLAJ6cib%mIviZnZ z=^FH0CqKh^)zG=}U@opr?93S#JQpsjlD_XIJ*I4x)^%0XEYlq_zAw!$A(g`c(76i< zGguDI`^}tDoNj!|6%JX)=NM19g0b38+M_cAJVz#!t5`C?=VGk`-^xBHWwAvu6xq0( zqa))&w)smWK^1#cSuL{@MHKu|`1hbLe`1VHb5sp8ZO2ii+Z zkYBgpp*_xY3m$xUyhfmDJ!d)3C9lKCaYTIw zD5u#q=KuR*cqz`A?o<~=rk-ZA3EU-s4m1GT_Z}d;+N}zZ)mNIbK4#$`3j_%gBuJ1T bK{A#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. --> - +