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-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index b13bb20b..8884c7bd 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -4,6 +4,7 @@
停止
设置
UDP 收集器: %1$s:%2$d
+ TCP 收集器: %1$s:%2$d
HTTP 服务器: http://%1$s:%2$d
%1$s 已接收 — %2$s 已发送
查询
@@ -22,10 +23,13 @@
持续时间
HTTP 服务器
UDP 导出器
+ TCP 导出器
+ TCP/UDP 导出器
无转储
不会转储流量
启动一个 HTTP 服务器下载 PCAP
发送 PCAP 到远程 UDP 接收器
+ 发送 PCAP 到远程 TCP 接收器(pcap-over-ip)
HTTP 服务器端口
收集器 IP 地址
收集器端口
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" />
-
+