1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
|
#pragma once
#include <pcap.h>
#include <atomic>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <optional>
#include <string>
#include <thread>
#include "wireframe/capture_queue.hpp"
#include "wireframe/filter.hpp"
#include "wireframe/pcapng/reader.hpp"
#include "wireframe/pcapng/writer.hpp"
#include "wireframe/privileges.hpp"
// Device-open -> datalink-validate -> filter/pcapng-setup -> signal-hook
// pipeline, shared by every frontend (CLI, TUI, GUI). Centralized so a
// new frontend can't silently skip a step the others rely on - e.g.
// the DLT_RAW/DLT_EN10MB check that summarize_packet() depends on, or
// the pcap_breakloop() shutdown hook that keeps a -w pcapng file from
// being truncated on Ctrl-C (see main.cpp's history: both were real
// bugs before this was centralized).
//
// Also covers replay mode (-r <file>): reading a previously-saved
// pcapng file back through the exact same queue/render/search pipeline
// as a live capture, so every frontend gets it for free rather than
// needing a second code path. The render/consumer side only ever talks
// to a CaptureQueue - it has no way to tell whether packets are
// arriving from a live pcap_loop or being read back from disk.
namespace wireframe {
namespace detail {
inline pcap_t* g_capture_handle = nullptr;
inline std::atomic<bool>* g_replay_stop_flag = nullptr;
inline void handle_stop_signal(int) {
if (g_capture_handle != nullptr) pcap_breakloop(g_capture_handle);
if (g_replay_stop_flag != nullptr) g_replay_stop_flag->store(true);
}
} // namespace detail
struct CaptureSessionOptions {
std::string device; // empty = pick the first device via pcap_findalldevs
std::optional<std::string> filter_expr;
std::optional<std::string> pcapng_output_path;
std::optional<std::string> replay_input_path; // -r: read from this pcapng file, not a live device
};
inline bool is_supported_datalink(int datalink) {
return datalink == DLT_EN10MB || datalink == DLT_RAW;
}
// Kernel/NIC-level counters, distinct from CaptureQueue::dropped():
// the queue can only count packets libpcap already handed to our
// callback. A traffic spike can drop packets in the kernel's capture
// buffer before that ever happens - invisible without this. Not
// meaningful in replay mode (stats() returns nullopt there).
struct CaptureStats {
unsigned int received; // ps_recv
unsigned int dropped; // ps_drop: kernel buffer had no room
unsigned int if_dropped; // ps_ifdrop: dropped by the interface/driver
};
class CaptureSession {
public:
~CaptureSession() { close(); }
CaptureSession() = default;
CaptureSession(const CaptureSession&) = delete;
CaptureSession& operator=(const CaptureSession&) = delete;
// Returns an error message on failure. The session remains safe to
// destroy (or close()) regardless of how far setup got.
std::optional<std::string> open(const CaptureSessionOptions& options) {
if (options.replay_input_path) {
if (options.filter_expr) {
return std::string(
"-f (capture filter) isn't supported with -r (replay); use -g to filter "
"what's displayed instead");
}
return open_replay(*options.replay_input_path, options.pcapng_output_path);
}
char errbuf[PCAP_ERRBUF_SIZE];
if (options.device.empty()) {
if (pcap_findalldevs(&all_devices_, errbuf) == -1 || all_devices_ == nullptr) {
return std::string("no capture device found: ") + errbuf;
}
device_ = all_devices_->name;
} else {
device_ = options.device;
}
handle_ = pcap_open_live(device_.c_str(), /*snaplen=*/65535, /*promisc=*/0,
/*to_ms=*/1000, errbuf);
if (handle_ == nullptr) {
return std::string("pcap_open_live failed: ") + errbuf;
}
// Everything CAP_NET_RAW/root was needed for is done: the
// handle is open. Drop immediately, before the datalink check
// or -w's file is even created - the latter is also why this
// runs this early rather than at the very end of open(), since
// it means a -w output file gets created as the real user, not
// root, and doesn't need a manual chown to read back afterward.
if (auto err = drop_privileges_if_root()) {
return "failed to drop root privileges after opening the capture handle: " + *err;
}
datalink_ = pcap_datalink(handle_);
if (!is_supported_datalink(datalink_)) {
return std::string("unsupported datalink type on ") + device_ + ": " +
pcap_datalink_val_to_name(datalink_) + " (" +
pcap_datalink_val_to_description(datalink_) + ")";
}
if (options.filter_expr) {
bpf_program program{};
if (auto err = compile_filter(handle_, *options.filter_expr, &program)) {
return "invalid filter '" + *options.filter_expr + "': " + *err;
}
if (pcap_setfilter(handle_, &program) == -1) {
std::string err = std::string("pcap_setfilter failed: ") + pcap_geterr(handle_);
pcap_freecode(&program);
return err;
}
pcap_freecode(&program); // bytecode is copied into the kernel by pcap_setfilter
}
if (options.pcapng_output_path) {
if (auto err = open_pcapng_writer(*options.pcapng_output_path)) return err;
}
return std::nullopt;
}
// pcap_loop() blocks in a read/poll waiting for the next packet, so
// a plain "stop requested" flag wouldn't unblock it promptly.
// pcap_breakloop() is documented as signal-safe and is what
// actually interrupts that wait. Replay mode has no handle to
// breakloop, so it's interrupted via g_replay_stop_flag instead --
// both are armed here so one signal handler covers either mode.
void install_signal_handlers() {
detail::g_capture_handle = handle_;
detail::g_replay_stop_flag = &replay_stop_requested_;
std::signal(SIGINT, detail::handle_stop_signal);
std::signal(SIGTERM, detail::handle_stop_signal);
}
void request_stop() {
if (handle_ != nullptr) pcap_breakloop(handle_);
replay_stop_requested_.store(true);
}
// True once a stop has been explicitly requested - via
// request_stop() or an external SIGINT/SIGTERM (the signal handler
// sets the same flag). Lets a frontend tell "the producer stopped
// because someone asked it to" apart from "the producer ran out of
// data on its own" (replay reaching end-of-file), which call for
// different UI behavior: the former should close the window, the
// latter should leave it open so what's already loaded can still be
// browsed.
bool stop_requested() const { return replay_stop_requested_.load(); }
// Must be called before close()/the destructor - pcap_stats()
// needs a still-open handle. Safe to call after request_stop(),
// since breakloop only stops pcap_loop(), it doesn't close handle_.
// Always nullopt in replay mode (handle_ is never set there).
std::optional<CaptureStats> stats() const {
if (handle_ == nullptr) return std::nullopt;
pcap_stat stat{};
if (pcap_stats(handle_, &stat) == -1) return std::nullopt;
return CaptureStats{stat.ps_recv, stat.ps_drop, stat.ps_ifdrop};
}
// Capture-thread side: copy each packet into the queue and return
// immediately. No decoding, printing, or file I/O here - that's
// every frontend's own consumer-side job.
//
// Live mode drops on backpressure (try_push, via capture_callback)
// since a traffic spike can't be paused. Replay mode blocks instead
// (push): a file has no real-time pressure forcing a drop, and
// dropping from what's supposed to be a faithful replay of a fixed
// historical record would defeat the point of replaying it.
std::thread start_capture_thread(CaptureQueue& queue) {
if (is_replay_) {
return std::thread([this, &queue] {
queue.push(to_captured_packet(std::move(*first_replay_packet_)));
while (!replay_stop_requested_.load()) {
auto record = replay_reader_->next_packet();
if (!record) break;
if (!queue.push(to_captured_packet(std::move(*record)))) break;
}
queue.stop();
});
}
return std::thread([this, &queue] {
pcap_loop(handle_, /*count=*/-1, capture_callback,
reinterpret_cast<unsigned char*>(&queue));
queue.stop();
});
}
void close() {
if (handle_ != nullptr) {
pcap_close(handle_);
handle_ = nullptr;
}
if (all_devices_ != nullptr) {
pcap_freealldevs(all_devices_);
all_devices_ = nullptr;
}
if (pcapng_file_ != nullptr) {
std::fclose(pcapng_file_);
pcapng_file_ = nullptr;
}
if (replay_file_ != nullptr) {
std::fclose(replay_file_);
replay_file_ = nullptr;
}
}
pcap_t* handle() const { return handle_; }
const std::string& device() const { return device_; }
int datalink() const { return datalink_; }
bool is_replay() const { return is_replay_; }
pcapng::Writer* pcapng_writer() { return pcapng_writer_ ? &*pcapng_writer_ : nullptr; }
private:
static void capture_callback(unsigned char* user, const pcap_pkthdr* header,
const unsigned char* raw) {
auto* queue = reinterpret_cast<CaptureQueue*>(user);
CapturedPacket packet;
packet.ts_sec = static_cast<std::uint32_t>(header->ts.tv_sec);
packet.ts_usec = static_cast<std::uint32_t>(header->ts.tv_usec);
packet.original_len = header->len;
packet.data.assign(raw, raw + header->caplen);
queue->try_push(std::move(packet));
}
static CapturedPacket to_captured_packet(pcapng::PacketRecord&& record) {
CapturedPacket packet;
packet.ts_sec = static_cast<std::uint32_t>(record.timestamp_us / 1'000'000ULL);
packet.ts_usec = static_cast<std::uint32_t>(record.timestamp_us % 1'000'000ULL);
packet.original_len = record.original_len;
packet.data = std::move(record.data);
return packet;
}
std::optional<std::string> open_pcapng_writer(const std::string& path) {
pcapng_file_ = std::fopen(path.c_str(), "wb");
if (pcapng_file_ == nullptr) {
return "failed to open " + path + " for writing: " + std::strerror(errno);
}
pcapng_writer_.emplace(pcapng_file_);
pcapng_writer_->write_section_header();
pcapng_writer_->write_interface_description(65535,
static_cast<std::uint16_t>(datalink_));
return std::nullopt;
}
std::optional<std::string> open_replay(const std::string& path,
const std::optional<std::string>& pcapng_output_path) {
replay_file_ = std::fopen(path.c_str(), "rb");
if (replay_file_ == nullptr) {
return "failed to open " + path + " for reading: " + std::strerror(errno);
}
replay_reader_.emplace(replay_file_);
// Reading the first packet is also what makes the reader consume
// the SHB/IDB blocks that precede it, which is what populates
// link_type() below - there's no separate "just read the
// header" step, so the packet itself is kept, not discarded.
first_replay_packet_ = replay_reader_->next_packet();
if (!first_replay_packet_) {
return "no packets found in " + path + " (empty, or not a valid pcapng file)";
}
auto link_type = replay_reader_->link_type();
if (!link_type || !is_supported_datalink(static_cast<int>(*link_type))) {
return "unsupported or missing link type in " + path;
}
datalink_ = static_cast<int>(*link_type);
device_ = path;
is_replay_ = true;
if (pcapng_output_path) {
if (auto err = open_pcapng_writer(*pcapng_output_path)) return err;
}
return std::nullopt;
}
pcap_t* handle_ = nullptr;
pcap_if_t* all_devices_ = nullptr;
std::string device_;
int datalink_ = 0;
std::FILE* pcapng_file_ = nullptr;
std::optional<pcapng::Writer> pcapng_writer_;
bool is_replay_ = false;
std::FILE* replay_file_ = nullptr;
std::optional<pcapng::Reader> replay_reader_;
std::optional<pcapng::PacketRecord> first_replay_packet_;
std::atomic<bool> replay_stop_requested_{false};
};
} // namespace wireframe
|