srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gui_main.cpp280
-rw-r--r--src/main.cpp326
2 files changed, 606 insertions, 0 deletions
diff --git a/src/gui_main.cpp b/src/gui_main.cpp
new file mode 100644
index 0000000..d5d4518
--- /dev/null
+++ b/src/gui_main.cpp
@@ -0,0 +1,280 @@
+// GUI frontend (secondary to the TUI - see PLAN.md Decisions). Same
+// capture/decode/filter/pcapng pipeline as main.cpp's CLI/TUI modes,
+// via wireframe::CaptureSession - not a hand-copied setup path, so it
+// can't drift on datalink validation, filter errors, or the
+// pcap_breakloop() shutdown hook the way two independent
+// implementations eventually would.
+//
+// Dear ImGui + SDL3 (see CMakeLists.txt for the toolkit decision).
+
+#include <SDL3/SDL.h>
+#include <imgui.h>
+#include <imgui_impl_sdl3.h>
+#include <imgui_impl_sdlrenderer3.h>
+
+#include <atomic>
+#include <cstdio>
+#include <cstring>
+#include <deque>
+#include <mutex>
+#include <optional>
+#include <span>
+#include <string>
+#include <thread>
+
+#include "wireframe/capture_session.hpp"
+#include "wireframe/search.hpp"
+#include "wireframe/summarize.hpp"
+
+namespace {
+
+struct PacketRow {
+ std::string summary;
+ std::vector<unsigned char> data;
+};
+
+constexpr std::size_t kMaxRows = 5000; // cap memory; oldest rows scroll off
+
+struct SharedState {
+ std::mutex mutex;
+ std::deque<PacketRow> rows;
+ std::uint64_t packet_count = 0;
+ bool replay_finished = false; // guarded by mutex, like rows/packet_count
+ // Set once the consumer loop drains and exits from an explicit stop
+ // (the window's quit action or an external SIGINT/SIGTERM) - but
+ // NOT when a replay simply reaches end-of-file on its own, since the
+ // point of replaying a file is browsing/searching it afterward, not
+ // watching it flash by and vanish. The render loop watches this so
+ // an external signal still closes the whole app in every other case
+ // - not just the capture, leaving a frozen window behind - the
+ // same single shutdown path run_tui() uses.
+ std::atomic<bool> capture_alive{true};
+};
+
+void consumer_loop(wireframe::CaptureSession& session, wireframe::CaptureQueue& queue,
+ SharedState& state) {
+ while (auto packet = queue.pop()) {
+ std::span<const unsigned char> bytes{packet->data};
+ std::string summary = wireframe::summarize_packet(bytes, session.datalink());
+
+ if (auto* writer = session.pcapng_writer()) {
+ writer->write_packet(/*interface_id=*/0, packet->ts_sec, packet->ts_usec, bytes,
+ packet->original_len);
+ }
+
+ std::lock_guard<std::mutex> lock(state.mutex);
+ state.rows.push_back({std::move(summary), std::move(packet->data)});
+ if (state.rows.size() > kMaxRows) state.rows.pop_front();
+ ++state.packet_count;
+ }
+ if (session.is_replay() && !session.stop_requested()) {
+ std::lock_guard<std::mutex> lock(state.mutex);
+ state.replay_finished = true;
+ } else {
+ state.capture_alive.store(false);
+ }
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ wireframe::CaptureSessionOptions options;
+ for (int i = 1; i < argc; ++i) {
+ if (std::strcmp(argv[i], "-w") == 0 && i + 1 < argc) {
+ options.pcapng_output_path = argv[++i];
+ } else if (std::strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
+ options.filter_expr = argv[++i];
+ } else if (std::strcmp(argv[i], "-r") == 0 && i + 1 < argc) {
+ options.replay_input_path = argv[++i];
+ } else if (options.device.empty()) {
+ options.device = argv[i];
+ }
+ }
+
+ wireframe::CaptureSession session;
+ if (auto err = session.open(options)) {
+ std::fprintf(stderr, "%s\n", err->c_str());
+ return 1;
+ }
+ session.install_signal_handlers();
+
+ if (!SDL_Init(SDL_INIT_VIDEO)) {
+ std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
+ return 1;
+ }
+
+ SDL_Window* window =
+ SDL_CreateWindow("wireframe", 1000, 650, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN);
+ if (window == nullptr) {
+ std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
+ SDL_Quit();
+ return 1;
+ }
+ SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
+ if (renderer == nullptr) {
+ std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+ SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+ SDL_ShowWindow(window);
+
+ IMGUI_CHECKVERSION();
+ ImGui::CreateContext();
+ ImGui::GetIO().IniFilename = nullptr; // no layout file: this is a fixed, simple layout
+ ImGui_ImplSDL3_InitForSDLRenderer(window, renderer);
+ ImGui_ImplSDLRenderer3_Init(renderer);
+
+ wireframe::CaptureQueue queue(4096);
+ SharedState state;
+ std::thread capture_thread = session.start_capture_thread(queue);
+ std::thread consumer_thread(consumer_loop, std::ref(session), std::ref(queue),
+ std::ref(state));
+
+ int selected_row = -1;
+ bool quit = false;
+ char search_buf[256] = {};
+ ImGuiIO& io = ImGui::GetIO();
+ while (!quit && state.capture_alive.load()) {
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ ImGui_ImplSDL3_ProcessEvent(&event);
+ if (event.type == SDL_EVENT_QUIT) {
+ quit = true;
+ session.request_stop();
+ }
+ // io.WantCaptureKeyboard reflects whether an ImGui widget
+ // (the search box) held keyboard focus as of the last
+ // completed frame - without this check, Escape would quit
+ // the whole app while the user is just trying to clear a
+ // search term, instead of doing what the TUI's Escape does
+ // in the same context (clear the term, stay open).
+ if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE &&
+ !io.WantCaptureKeyboard) {
+ quit = true;
+ session.request_stop();
+ }
+ }
+
+ ImGui_ImplSDLRenderer3_NewFrame();
+ ImGui_ImplSDL3_NewFrame();
+ ImGui::NewFrame();
+
+ ImGui::SetNextWindowPos(ImVec2(0, 0));
+ ImGui::SetNextWindowSize(io.DisplaySize);
+ ImGui::Begin("wireframe", nullptr,
+ ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
+ ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse);
+
+ if (session.is_replay()) {
+ ImGui::Text("replaying %s (%s)", session.device().c_str(),
+ pcap_datalink_val_to_name(session.datalink()));
+ } else {
+ ImGui::Text("capturing on %s (%s)", session.device().c_str(),
+ pcap_datalink_val_to_name(session.datalink()));
+ }
+ ImGui::SameLine();
+ ImGui::SetNextItemWidth(300);
+ ImGui::InputTextWithHint("##search", "search (display filter, not capture filter)",
+ search_buf, sizeof(search_buf));
+ if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
+ search_buf[0] = '\0'; // same behavior as the TUI's Esc-while-searching
+ }
+ std::string search_term(search_buf);
+ ImGui::Separator();
+
+ float details_height = 160.0f;
+ std::size_t shown = 0;
+ ImGui::BeginChild("packet_list", ImVec2(0, -details_height - 8), ImGuiChildFlags_Borders);
+ {
+ std::lock_guard<std::mutex> lock(state.mutex);
+ for (std::size_t i = 0; i < state.rows.size(); ++i) {
+ if (!wireframe::matches_search(state.rows[i].summary, search_term)) continue;
+ ++shown;
+
+ // ImGui derives a widget's ID from its label text by
+ // default; two rows with identical summary text (e.g.
+ // repeated ICMP lines) would otherwise collide on the
+ // same ID. PushID(index) makes each row's ID unique
+ // regardless of what text it displays.
+ ImGui::PushID(static_cast<int>(i));
+ bool is_selected = (selected_row == static_cast<int>(i));
+ if (ImGui::Selectable(state.rows[i].summary.c_str(), is_selected)) {
+ selected_row = static_cast<int>(i);
+ }
+ ImGui::PopID();
+ }
+ // Auto-scroll to the newest row unless the user has scrolled up
+ // to look at something (a manual scroll leaves the view short
+ // of the max, which is what we check here).
+ if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f) {
+ ImGui::SetScrollHereY(1.0f);
+ }
+ }
+ ImGui::EndChild();
+
+ ImGui::BeginChild("packet_details", ImVec2(0, details_height), ImGuiChildFlags_Borders);
+ {
+ std::lock_guard<std::mutex> lock(state.mutex);
+ if (selected_row >= 0 && selected_row < static_cast<int>(state.rows.size())) {
+ for (const auto& line : wireframe::hex_dump_lines(state.rows[selected_row].data)) {
+ ImGui::TextUnformatted(line.c_str());
+ }
+ } else {
+ ImGui::TextDisabled("select a packet to see its hex dump");
+ }
+ }
+ ImGui::EndChild();
+
+ ImGui::Separator();
+ {
+ std::lock_guard<std::mutex> lock(state.mutex);
+ const char* finished = state.replay_finished ? " [replay finished]" : "";
+ if (search_term.empty()) {
+ ImGui::Text("packets: %llu%s dropped: %llu (Esc to quit)",
+ static_cast<unsigned long long>(state.packet_count), finished,
+ static_cast<unsigned long long>(queue.dropped()));
+ } else {
+ ImGui::Text("packets: %llu (%zu shown)%s dropped: %llu (Esc to clear search)",
+ static_cast<unsigned long long>(state.packet_count), shown, finished,
+ static_cast<unsigned long long>(queue.dropped()));
+ }
+ }
+ // Kernel/interface-level drops: a traffic spike can drop
+ // packets before libpcap ever hands them to our callback,
+ // which the queue-side counter above can't see.
+ if (auto stats = session.stats()) {
+ if (stats->dropped > 0 || stats->if_dropped > 0) {
+ ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f),
+ "kernel/interface dropped %u/%u (received %u)", stats->dropped,
+ stats->if_dropped, stats->received);
+ }
+ }
+
+ ImGui::End();
+
+ ImGui::Render();
+ SDL_SetRenderDrawColor(renderer, 30, 30, 30, 255);
+ SDL_RenderClear(renderer);
+ ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), renderer);
+ SDL_RenderPresent(renderer);
+ }
+
+ session.request_stop();
+ capture_thread.join();
+ consumer_thread.join();
+
+ if (queue.dropped() > 0) {
+ std::fprintf(stderr, "dropped %llu packets (render side fell behind)\n",
+ static_cast<unsigned long long>(queue.dropped()));
+ }
+
+ ImGui_ImplSDLRenderer3_Shutdown();
+ ImGui_ImplSDL3_Shutdown();
+ ImGui::DestroyContext();
+ SDL_DestroyRenderer(renderer);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 0;
+}
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..3a3e925
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,326 @@
+// Stage 2 (PLAN.md): Ethernet/IP/TCP/UDP decoders producing a live
+// packet-list line per capture. The TUI itself is still an open
+// question (PLAN.md), so this prints to stdout for now; -x keeps the
+// stage-1 hex dump available underneath each summary.
+//
+// Stage 3: -w <file> writes the same capture out as pcapng alongside
+// the summary, so files stay Wireshark-compatible (PLAN.md).
+//
+// Stage 4: capture thread -> bounded CaptureQueue -> render loop on
+// the main thread (PLAN.md's architecture sketch). The capture thread
+// only copies raw bytes into the queue; decoding, printing, and
+// pcapng-writing all happen on the consumer side, so a slow render
+// path can never block the capture thread - a full queue drops the
+// packet and counts it instead.
+//
+// Stage 5: L7 dissectors register into an L7Registry keyed by port
+// (wireframe/l7/dissector.hpp) and get consulted from summarize_packet
+// once TCP/UDP decode a port number. DNS is the first one, proving the
+// interface against real traffic rather than synthetic bytes.
+//
+// IPv6: dispatched by version nibble rather than assumed absent --
+// every live-capture test so far has shown real IPv6 background
+// traffic silently dropped once the decoder only handled IPv4.
+//
+// Stage 6: -f <expr> compiles a tcpdump-style BPF expression via
+// libpcap's own compiler (wireframe/filter.hpp) and installs it with
+// pcap_setfilter(), filtering in the kernel before packets ever reach
+// userspace - rather than hand-rolling a second BPF parser.
+//
+// Device-open/datalink-validate/filter/pcapng/signal-handler setup all
+// goes through wireframe::CaptureSession (wireframe/capture_session.hpp)
+// - the same one gui_main.cpp uses - so the CLI/TUI and GUI frontends
+// can't drift apart on that setup path.
+
+#include <pcap.h>
+
+#include <cstdio>
+#include <cstring>
+#include <deque>
+#include <mutex>
+#include <optional>
+#include <span>
+#include <string>
+#include <thread>
+
+#include <ftxui/component/component.hpp>
+#include <ftxui/component/screen_interactive.hpp>
+#include <ftxui/dom/elements.hpp>
+
+#include "wireframe/capture_session.hpp"
+#include "wireframe/search.hpp"
+#include "wireframe/summarize.hpp"
+
+namespace {
+
+// Queue capacity: how many packets can be buffered between the capture
+// thread and the render loop before new packets get dropped. Sized as
+// a fixed constant rather than a flag - tune later if a real workload
+// needs it, not speculatively now.
+constexpr std::size_t kQueueCapacity = 4096;
+
+void hex_dump(std::span<const unsigned char> bytes) {
+ for (const auto& line : wireframe::hex_dump_lines(bytes)) {
+ std::printf("%s\n", line.c_str());
+ }
+ std::printf("\n");
+}
+
+struct RenderOptions {
+ bool verbose_hex;
+ int datalink;
+ wireframe::pcapng::Writer* pcapng_writer;
+ std::string search_term; // display filter - see wireframe/search.hpp
+};
+
+void render_packet(const wireframe::CapturedPacket& packet, const RenderOptions& opts) {
+ std::span<const unsigned char> bytes{packet.data};
+
+ std::string line = wireframe::summarize_packet(bytes, opts.datalink);
+
+ // -g is a display filter, not a capture filter: still written to
+ // -w regardless of whether it matches, since -w should reflect
+ // what was actually captured (that's -f's job), not what's shown.
+ if (opts.pcapng_writer) {
+ opts.pcapng_writer->write_packet(/*interface_id=*/0, packet.ts_sec, packet.ts_usec, bytes,
+ packet.original_len);
+ }
+
+ if (!wireframe::matches_search(line, opts.search_term)) return;
+
+ std::printf("%s\n", line.c_str());
+ if (opts.verbose_hex) hex_dump(bytes);
+
+ // Flush per packet: stdout is fully buffered off a tty, and this is
+ // a live capture tool, not a batch one.
+ std::fflush(stdout);
+}
+
+// TUI mode (-t): a scrolling packet list in a full-screen view, built
+// with FTXUI (see NAMES.md-adjacent decision: chosen over notcurses for
+// pure-C++ portability - no C build-system/dependency chain to fight
+// on every platform PLAN.md targets, and genuine Windows console
+// support, which notcurses lacks).
+//
+// A dedicated consumer thread pops from the capture queue and appends
+// formatted rows to shared state; the UI thread just redraws on
+// Event::Custom. 'q'/Esc triggers the same pcap_breakloop() shutdown
+// path as Ctrl-C, so there's one shutdown sequence, not two: breakloop
+// -> capture thread's pcap_loop returns -> queue.stop() -> consumer
+// drains and calls screen.Exit() -> screen.Loop() returns.
+void run_tui(wireframe::CaptureSession& session, wireframe::CaptureQueue& queue,
+ RenderOptions& opts) {
+ using namespace ftxui;
+
+ constexpr std::size_t kMaxRows = 2000; // cap memory; oldest rows scroll off
+
+ std::mutex state_mutex;
+ std::deque<std::string> rows;
+ std::uint64_t packet_count = 0;
+
+ // '/' search: a display filter over `rows`, independent of the
+ // capture itself (wireframe/search.hpp) - typed and read only on
+ // the UI thread (the consumer thread never touches it), so unlike
+ // `rows`/`packet_count` it doesn't need state_mutex.
+ bool searching = false;
+ std::string search_term;
+ bool replay_finished = false; // guarded by state_mutex, like rows/packet_count
+
+ auto screen = ScreenInteractive::Fullscreen();
+
+ std::thread consumer_thread([&] {
+ while (auto packet = queue.pop()) {
+ std::span<const unsigned char> bytes{packet->data};
+ std::string line = wireframe::summarize_packet(bytes, opts.datalink);
+
+ if (opts.pcapng_writer) {
+ opts.pcapng_writer->write_packet(/*interface_id=*/0, packet->ts_sec,
+ packet->ts_usec, bytes, packet->original_len);
+ }
+
+ {
+ std::lock_guard<std::mutex> lock(state_mutex);
+ rows.push_back(std::move(line));
+ if (rows.size() > kMaxRows) rows.pop_front();
+ ++packet_count;
+ }
+ screen.PostEvent(Event::Custom);
+ }
+ // Replay reaching end-of-file on its own (stop_requested() still
+ // false) shouldn't close the window - the point of replaying a
+ // file is browsing/searching it afterward, not watching it flash
+ // by. An explicit stop (q/Esc below, or an external signal, both
+ // of which set stop_requested()) always closes, live capture
+ // included - that's still the same behavior as before.
+ if (session.is_replay() && !session.stop_requested()) {
+ {
+ std::lock_guard<std::mutex> lock(state_mutex);
+ replay_finished = true;
+ }
+ screen.PostEvent(Event::Custom); // one more redraw for the final state
+ } else {
+ screen.Exit();
+ }
+ });
+
+ auto renderer = Renderer([&] {
+ std::lock_guard<std::mutex> lock(state_mutex);
+ Elements lines;
+ std::size_t shown = 0;
+ for (const auto& row : rows) {
+ if (!wireframe::matches_search(row, search_term)) continue;
+ lines.push_back(text(row));
+ ++shown;
+ }
+
+ std::string status = "packets: " + std::to_string(packet_count);
+ if (!search_term.empty()) status += " (" + std::to_string(shown) + " shown)";
+ if (replay_finished) status += " [replay finished]";
+ status += " dropped: " + std::to_string(queue.dropped()) +
+ (searching ? " (Enter to apply, Esc to clear)" : " (/ to search, q to quit)");
+ // Kernel/interface-level drops: a traffic spike can drop
+ // packets before libpcap ever hands them to our callback,
+ // which the queue-side counter above can't see.
+ Elements footer = {text(status) | dim};
+ if (auto stats = session.stats()) {
+ if (stats->dropped > 0 || stats->if_dropped > 0) {
+ std::string kernel_status = "kernel/interface dropped " +
+ std::to_string(stats->dropped) + "/" +
+ std::to_string(stats->if_dropped) + " (received " +
+ std::to_string(stats->received) + ")";
+ footer.push_back(text(kernel_status) | color(Color::Yellow));
+ }
+ }
+ if (searching || !search_term.empty()) {
+ footer.push_back(text("search: " + search_term + (searching ? "_" : "")) |
+ color(Color::Green));
+ }
+ std::string title = session.is_replay() ? ("wireframe - replaying " + session.device())
+ : "wireframe - live capture";
+ return vbox({
+ text(title) | bold | color(Color::Cyan),
+ separator(),
+ vbox(std::move(lines)) | yframe | flex,
+ separator(),
+ vbox(std::move(footer)),
+ }) |
+ border;
+ });
+
+ auto component = CatchEvent(renderer, [&](const Event& event) {
+ if (searching) {
+ if (event == Event::Return) {
+ searching = false;
+ return true;
+ }
+ if (event == Event::Escape) {
+ searching = false;
+ search_term.clear();
+ return true;
+ }
+ if (event == Event::Backspace) {
+ if (!search_term.empty()) search_term.pop_back();
+ return true;
+ }
+ if (event.is_character()) {
+ search_term += event.character();
+ return true;
+ }
+ return true; // swallow anything else while typing (don't let it fall through to quit)
+ }
+ if (event == Event::Character('/')) {
+ searching = true;
+ return true;
+ }
+ if (event == Event::Character('q') || event == Event::Escape) {
+ session.request_stop();
+ // Closes immediately rather than waiting for the capture/
+ // replay thread to actually finish and drain the queue --
+ // necessary for replay mode specifically (that thread may
+ // already be long gone once the user quits after browsing a
+ // finished replay, so nothing else would ever call this).
+ // main() still joins the thread properly afterward either way.
+ screen.Exit();
+ return true;
+ }
+ return false;
+ });
+
+ screen.Loop(component);
+ consumer_thread.join();
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ wireframe::CaptureSessionOptions options;
+ bool tui_mode = false;
+ RenderOptions opts{
+ .verbose_hex = false, .datalink = 0, .pcapng_writer = nullptr, .search_term = ""};
+
+ for (int i = 1; i < argc; ++i) {
+ if (std::strcmp(argv[i], "-x") == 0) {
+ opts.verbose_hex = true;
+ } else if (std::strcmp(argv[i], "-t") == 0 || std::strcmp(argv[i], "--tui") == 0) {
+ tui_mode = true;
+ } else if (std::strcmp(argv[i], "-w") == 0 && i + 1 < argc) {
+ options.pcapng_output_path = argv[++i];
+ } else if (std::strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
+ options.filter_expr = argv[++i];
+ } else if (std::strcmp(argv[i], "-r") == 0 && i + 1 < argc) {
+ options.replay_input_path = argv[++i];
+ } else if (std::strcmp(argv[i], "-g") == 0 && i + 1 < argc) {
+ opts.search_term = argv[++i];
+ } else if (options.device.empty()) {
+ options.device = argv[i];
+ }
+ }
+
+ wireframe::CaptureSession session;
+ if (auto err = session.open(options)) {
+ std::fprintf(stderr, "%s\n", err->c_str());
+ return 1;
+ }
+ opts.datalink = session.datalink();
+ opts.pcapng_writer = session.pcapng_writer();
+ session.install_signal_handlers();
+
+ if (!tui_mode) {
+ if (session.is_replay()) {
+ std::printf("replaying %s (%s)\n", session.device().c_str(),
+ pcap_datalink_val_to_name(session.datalink()));
+ } else {
+ std::printf("capturing on %s (%s, ctrl-c to stop)\n", session.device().c_str(),
+ pcap_datalink_val_to_name(session.datalink()));
+ }
+ }
+
+ wireframe::CaptureQueue queue(kQueueCapacity);
+ std::thread capture_thread = session.start_capture_thread(queue);
+
+ if (tui_mode) {
+ run_tui(session, queue, opts);
+ } else {
+ while (auto packet = queue.pop()) {
+ render_packet(*packet, opts);
+ }
+ }
+
+ capture_thread.join();
+
+ if (queue.dropped() > 0) {
+ std::fprintf(stderr, "dropped %llu packets (render side fell behind)\n",
+ static_cast<unsigned long long>(queue.dropped()));
+ }
+ // Kernel-level counters, queried before the session closes its
+ // handle: a traffic spike can drop packets before libpcap ever
+ // hands them to our callback, which queue.dropped() can't see.
+ if (auto stats = session.stats()) {
+ if (stats->dropped > 0 || stats->if_dropped > 0) {
+ std::fprintf(stderr, "kernel/interface dropped %u/%u packets (received %u)\n",
+ stats->dropped, stats->if_dropped, stats->received);
+ }
+ }
+
+ return 0;
+}