From 91f258d8abc7ba233094bcc618a016d70ae2cb24 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Fri, 25 Apr 2025 18:34:35 +0200 Subject: [PATCH] Implement TCP exporter (pcap-over-ip) It makes it possible to integrate PCAPdroid with other software See #509 --- .../remote_capture/CaptureService.java | 32 +++++-- .../activities/CaptureCtrl.java | 5 +- .../activities/MainActivity.java | 5 +- .../activities/prefs/SettingsActivity.java | 4 +- .../fragments/StatusFragment.java | 4 + .../emanuelef/remote_capture/model/Prefs.java | 5 +- .../remote_capture/pcap_dump/TCPDumper.java | 95 +++++++++++++++++++ .../remote_capture/pcap_dump/UDPDumper.java | 19 ++++ app/src/main/res/values/arrays.xml | 3 + app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/root_preferences.xml | 2 +- docs/app_api.md | 6 +- 12 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/emanuelef/remote_capture/pcap_dump/TCPDumper.java 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 ab80c9d0..099dddab 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -79,6 +79,7 @@ import com.emanuelef.remote_capture.model.CaptureStats; import com.emanuelef.remote_capture.pcap_dump.FileDumper; import com.emanuelef.remote_capture.pcap_dump.HTTPServer; import com.emanuelef.remote_capture.interfaces.PcapDumper; +import com.emanuelef.remote_capture.pcap_dump.TCPDumper; import com.emanuelef.remote_capture.pcap_dump.UDPDumper; import com.pcapdroid.mitm.MitmAPI; @@ -385,22 +386,24 @@ public class CaptureService extends VpnService implements Runnable { } mDumper = new UDPDumper(new InetSocketAddress(addr, mSettings.collector_port), mSettings.pcapng_format); - } - - if(mDumper != null) { - // Max memory usage = (JAVA_PCAP_BUFFER_SIZE * 64) = 32 MB - mDumpQueue = new LinkedBlockingDeque<>(64); + } else if(mSettings.dump_mode == Prefs.DumpMode.TCP_EXPORTER) { + InetAddress addr; try { - mDumper.startDumper(); - } catch (IOException | SecurityException e) { + addr = InetAddress.getByName(mSettings.collector_address); + } catch (UnknownHostException e) { reportError(e.getLocalizedMessage()); e.printStackTrace(); - mDumper = null; return abortStart(); } + + mDumper = new TCPDumper(new InetSocketAddress(addr, mSettings.collector_port), mSettings.pcapng_format); } + if(mDumper != null) + // Max memory usage = (JAVA_PCAP_BUFFER_SIZE * 64) = 32 MB + mDumpQueue = new LinkedBlockingDeque<>(64); + mSocks5Address = ""; mSocks5Enabled = mSettings.socks5_enabled || mSettings.tls_decryption; if(mSocks5Enabled) { @@ -1230,6 +1233,19 @@ public class CaptureService extends VpnService implements Runnable { } private void dumpWork() { + Log.d(TAG, "Starting the dumper"); + + try { + mDumper.startDumper(); + } catch (IOException | SecurityException e) { + e.printStackTrace(); + reportError(e.getLocalizedMessage()); + mHandler.post(CaptureService::stopPacketLoop); + return; + } + + Log.d(TAG, "Dumper running"); + while(true) { byte[] data; try { diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java index 65dc8da4..f80eb711 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java @@ -220,7 +220,10 @@ public class CaptureCtrl extends AppCompatActivity { private String checkRemoteServerNotAllowed(CaptureSettings settings) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - if((settings.dump_mode == Prefs.DumpMode.UDP_EXPORTER) && + boolean exporterEnabled = (settings.dump_mode == Prefs.DumpMode.UDP_EXPORTER) || + (settings.dump_mode == Prefs.DumpMode.TCP_EXPORTER); + + if(exporterEnabled && !Utils.isLocalNetworkAddress(settings.collector_address) && !Prefs.getCollectorIp(prefs).equals(settings.collector_address)) return settings.collector_address; diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java index cf46ae51..a0991299 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java @@ -824,7 +824,10 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig if(mPrefs.getBoolean(Prefs.PREF_REMOTE_COLLECTOR_ACK, false)) return false; // already acknowledged - if(((Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.UDP_EXPORTER) && !Utils.isLocalNetworkAddress(Prefs.getCollectorIp(mPrefs))) || + boolean exporterEnabled = (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.UDP_EXPORTER) || + (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.TCP_EXPORTER); + + if((exporterEnabled && !Utils.isLocalNetworkAddress(Prefs.getCollectorIp(mPrefs))) || (Prefs.getSocks5Enabled(mPrefs) && !Utils.isLocalNetworkAddress(Prefs.getSocks5ProxyHost(mPrefs)))) { Log.i(TAG, "Showing possible scan notice"); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java index dce37c23..2b77d271 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java @@ -202,7 +202,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment setPreferencesFromResource(R.xml.root_preferences, rootKey); mIab = Billing.newInstance(requireContext()); - setupUdpExporterPrefs(); + setupExporterPrefs(); setupHttpServerPrefs(); setupTrafficInspectionPrefs(); setupCapturePrefs(); @@ -254,7 +254,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment } @SuppressWarnings("deprecation") - private void setupUdpExporterPrefs() { + private void setupExporterPrefs() { /* Collector IP validation */ EditTextPreference mRemoteCollectorIp = requirePreference(Prefs.PREF_COLLECTOR_IP_KEY); mRemoteCollectorIp.setOnPreferenceChangeListener((preference, newValue) -> Utils.validateIpAddress(newValue.toString())); diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java index 265e7bf0..45c82f22 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java @@ -290,6 +290,10 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr info = String.format(getResources().getString(R.string.collector_info), CaptureService.getCollectorAddress(), CaptureService.getCollectorPort()); break; + case TCP_EXPORTER: + info = String.format(getResources().getString(R.string.tcp_collector_info), + CaptureService.getCollectorAddress(), CaptureService.getCollectorPort()); + break; } mCollectorInfoText.setText(info); diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java index f8f5f6c4..ad1451af 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java @@ -38,6 +38,7 @@ public class Prefs { public static final String DUMP_NONE = "none"; public static final String DUMP_HTTP_SERVER = "http_server"; public static final String DUMP_UDP_EXPORTER = "udp_exporter"; + public static final String DUMP_TCP_EXPORTER = "tcp_exporter"; public static final String DUMP_PCAP_FILE = "pcap_file"; public static final String DEFAULT_DUMP_MODE = DUMP_NONE; @@ -114,7 +115,8 @@ public class Prefs { NONE, HTTP_SERVER, PCAP_FILE, - UDP_EXPORTER + UDP_EXPORTER, + TCP_EXPORTER } public enum IpMode { @@ -140,6 +142,7 @@ public class Prefs { case DUMP_HTTP_SERVER: return DumpMode.HTTP_SERVER; case DUMP_PCAP_FILE: return DumpMode.PCAP_FILE; case DUMP_UDP_EXPORTER: return DumpMode.UDP_EXPORTER; + case DUMP_TCP_EXPORTER: return DumpMode.TCP_EXPORTER; default: return DumpMode.NONE; } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/TCPDumper.java b/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/TCPDumper.java new file mode 100644 index 00000000..641aa8e4 --- /dev/null +++ b/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/TCPDumper.java @@ -0,0 +1,95 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid 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. + * + * PCAPdroid 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 PCAPdroid. If not, see . + * + * Copyright 2020-25 - Emanuele Faranda + */ + +package com.emanuelef.remote_capture.pcap_dump; + +import com.emanuelef.remote_capture.CaptureService; +import com.emanuelef.remote_capture.Utils; +import com.emanuelef.remote_capture.interfaces.PcapDumper; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Iterator; + +public class TCPDumper implements PcapDumper { + private static final String TAG = "TCPDumper"; + private final InetSocketAddress mServer; + private final boolean mPcapngFormat; + private boolean mSendHeader; + private Socket mSocket; + private DataOutputStream mDataOut; + + public TCPDumper(InetSocketAddress server, boolean pcapngFormat) { + mServer = server; + mSendHeader = true; + mPcapngFormat = pcapngFormat; + } + + @Override + public void startDumper() throws IOException { + mSocket = new Socket(); + boolean ok = false; + + try { + mSocket.connect(mServer, 1000); + mDataOut = new DataOutputStream(mSocket.getOutputStream()); + ok = true; + } finally { + if (!ok) + mSocket.close(); + } + + CaptureService.requireInstance().protect(mSocket); + } + + @Override + public void stopDumper() throws IOException { + try { + mDataOut.close(); + } finally { + mSocket.close(); + } + } + + @Override + public String getBpf() { + return "not (host " + mServer.getAddress().getHostAddress() + " and tcp port " + mServer.getPort() + ")"; + } + + @Override + public void dumpData(byte[] data) throws IOException { + if(mSendHeader) { + mSendHeader = false; + + byte[] hdr = CaptureService.getPcapHeader(); + mDataOut.write(hdr); + } + + Iterator it = Utils.iterPcapRecords(data, mPcapngFormat); + int pos = 0; + + while(it.hasNext()) { + int rec_len = it.next(); + mDataOut.write(data, pos, rec_len); + pos += rec_len; + } + } +} diff --git a/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/UDPDumper.java b/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/UDPDumper.java index 82286ff8..7f68f080 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/UDPDumper.java +++ b/app/src/main/java/com/emanuelef/remote_capture/pcap_dump/UDPDumper.java @@ -1,3 +1,22 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid 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. + * + * PCAPdroid 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 PCAPdroid. If not, see . + * + * Copyright 2020-25 - Emanuele Faranda + */ + package com.emanuelef.remote_capture.pcap_dump; import com.emanuelef.remote_capture.CaptureService; diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 605c49c9..ea202b85 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -5,18 +5,21 @@ http_server pcap_file udp_exporter + tcp_exporter @string/no_dump @string/http_server @string/pcap_file @string/udp_exporter + @string/tcp_exporter @string/no_dump_info @string/http_server_info @string/pcap_file_info @string/udp_exporter_info + @string/tcp_exporter_info diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2dcf4dd7..2e599b6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Stop Settings UDP collector: %1$s:%2$d + TCP collector: %1$s:%2$d HTTP server: http://%1$s:%2$d %1$s received — %2$s sent Query @@ -69,10 +70,13 @@ Duration HTTP server UDP exporter + TCP exporter + TCP/UDP exporter No dump Traffic will not be dumped Start an HTTP server for the PCAP download - Sends the PCAP to a remote UDP receiver + Send the PCAP to a remote UDP receiver + Send the PCAP to a remote TCP receiver (pcap-over-ip) HTTP server port Collector IP address Collector port diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index b27db756..ebb29c5b 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -28,7 +28,7 @@ app:useSimpleSummaryProvider="true" /> - +