Merge pull request #513 from emanuele-f/issue-509-tcp-exporter
Some checks failed
Debug build / build (push) Has been cancelled
Validate Gradle Wrapper / Validation (push) Has been cancelled
Test native code / test (push) Has been cancelled
Windows build / build (push) Has been cancelled

Implement TCP Exporter (pcap-over-ip)
This commit is contained in:
Emanuele Faranda 2025-04-27 20:14:56 +02:00 committed by GitHub
commit b098d65515
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 172 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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<Integer> 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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Copyright 2020-25 - Emanuele Faranda
*/
package com.emanuelef.remote_capture.pcap_dump;
import com.emanuelef.remote_capture.CaptureService;

View File

@ -4,6 +4,7 @@
<string name="stop_button">停止</string>
<string name="title_activity_settings">设置</string>
<string name="collector_info">UDP 收集器: %1$s:%2$d</string>
<string name="tcp_collector_info">TCP 收集器: %1$s:%2$d</string>
<string name="http_server_status">HTTP 服务器: http://%1$s:%2$d</string>
<string name="rcvd_and_sent">%1$s 已接收 — %2$s 已发送</string>
<string name="query">查询</string>
@ -22,10 +23,13 @@
<string name="duration">持续时间</string>
<string name="http_server">HTTP 服务器</string>
<string name="udp_exporter">UDP 导出器</string>
<string name="tcp_exporter">TCP 导出器</string>
<string name="tcp_udp_exporter">TCP/UDP 导出器</string>
<string name="no_dump">无转储</string>
<string name="no_dump_info">不会转储流量</string>
<string name="http_server_info">启动一个 HTTP 服务器下载 PCAP</string>
<string name="udp_exporter_info">发送 PCAP 到远程 UDP 接收器</string>
<string name="tcp_exporter_info">发送 PCAP 到远程 TCP 接收器pcap-over-ip</string>
<string name="http_server_port">HTTP 服务器端口</string>
<string name="receiver_ip_address">收集器 IP 地址</string>
<string name="receiver_port">收集器端口</string>

View File

@ -5,18 +5,21 @@
<item>http_server</item>
<item>pcap_file</item>
<item>udp_exporter</item>
<item>tcp_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>
<item>@string/tcp_exporter</item>
</string-array>
<string-array name="pcap_dump_modes_descriptions">
<item>@string/no_dump_info</item>
<item>@string/http_server_info</item>
<item>@string/pcap_file_info</item>
<item>@string/udp_exporter_info</item>
<item>@string/tcp_exporter_info</item>
</string-array>
<string-array name="app_languages">

View File

@ -51,6 +51,7 @@
<string name="stop_button">Stop</string>
<string name="title_activity_settings">Settings</string>
<string name="collector_info">UDP collector: %1$s:%2$d</string>
<string name="tcp_collector_info">TCP collector: %1$s:%2$d</string>
<string name="http_server_status">HTTP server: http://%1$s:%2$d</string>
<string name="rcvd_and_sent">%1$s received — %2$s sent</string>
<string name="query">Query</string>
@ -69,10 +70,13 @@
<string name="duration">Duration</string>
<string name="http_server">HTTP server</string>
<string name="udp_exporter">UDP exporter</string>
<string name="tcp_exporter">TCP exporter</string>
<string name="tcp_udp_exporter">TCP/UDP exporter</string>
<string name="no_dump">No dump</string>
<string name="no_dump_info">Traffic 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 a remote UDP receiver</string>
<string name="udp_exporter_info">Send the PCAP to a remote UDP receiver</string>
<string name="tcp_exporter_info">Send the PCAP to a remote TCP receiver (pcap-over-ip)</string>
<string name="http_server_port">HTTP server port</string>
<string name="receiver_ip_address">Collector IP address</string>
<string name="receiver_port">Collector port</string>

View File

@ -28,7 +28,7 @@
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/udp_exporter" app:iconSpaceReserved="false">
<PreferenceCategory app:title="@string/tcp_udp_exporter" app:iconSpaceReserved="false">
<EditTextPreference
app:key="collector_ip_address"
app:title="@string/receiver_ip_address"

View File

@ -78,10 +78,10 @@ As shown above, the capture settings can be specified by using intent extras. Th
| Parameter | Type | Ver | Mode | Value |
|-------------------------|--------|-----|------|--------------------------------------------------------------------|
| pcap_dump_mode | string | | | none \| http_server \| udp_exporter \| pcap_file |
| pcap_dump_mode | string | | | none \| http_server \| udp_exporter \| tcp_exporter \| pcap_file |
| app_filter | string | | | package name of the app(s) to capture (73+: comma separated list) |
| collector_ip_address | string | | | the IP address of the collector in udp_exporter mode |
| collector_port | int | | | the UDP port of the collector in udp_exporter mode |
| collector_ip_address | string | | | the IP address of the collector in tcp/udp_exporter mode |
| collector_port | int | | | the UDP port of the collector in tcp/udp_exporter mode |
| http_server_port | int | | | the HTTP server port in http_server mode |
| pcap_uri | string | | | the URI for the PCAP dump in pcap_file mode (overrides pcap_name) |
| socks5_enabled | bool | | vpn | true to redirect the TCP connections to a SOCKS5 proxy |