srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/src/gui_main.cpp
blob: 8a7771af03190ad2316db546ade424b5c0ce7ba9 (plain) (blame)
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
// 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/net/tcp_reassembly.hpp"
#include "wireframe/packet_diagnostics.hpp"
#include "wireframe/search.hpp"
#include "wireframe/summarize.hpp"

namespace {

struct PacketRow {
    std::string summary;
    std::vector<unsigned char> data;
    // -a only: computed once at consume time (reassembly needs
    // in-order state across packets, unlike checksum status below,
    // which is stateless and cheap enough to compute lazily when a row
    // is selected instead of storing it on every row).
    std::optional<std::string> reassembled_http;
};

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, wireframe::net::TcpReassembler* reassembler) {
    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::optional<std::string> reassembled_http;
        if (reassembler) {
            reassembled_http = wireframe::reassembled_http_status(bytes, session.datalink(),
                                                                    *reassembler);
        }

        std::lock_guard<std::mutex> lock(state.mutex);
        state.rows.push_back({std::move(summary), std::move(packet->data),
                               std::move(reassembled_http)});
        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);
    }
}

void print_usage(const char* argv0) {
    std::printf(
        "wireframe - terminal packet capture and analysis tool (GUI)\n"
        "\n"
        "Usage: %s [options] [interface]\n"
        "\n"
        "If no interface is given, the first available device is used.\n"
        "Search is available interactively in the window itself.\n"
        "\n"
        "Options:\n"
        "  -w <file>       Write the capture to <file> as pcapng (Wireshark-compatible)\n"
        "  -r <file>       Replay a saved pcapng file instead of a live device\n"
        "  -f <expr>       Kernel-level capture filter (tcpdump/BPF syntax); also\n"
        "                  applies to what -w writes. Can't be combined with -r.\n"
        "  -c              Show IPv4/TCP/UDP checksum validity for the selected packet.\n"
        "                  Off by default - see the CLI's -h for why (checksum offload).\n"
        "  -a              Reassemble TCP streams and show HTTP requests/responses\n"
        "                  joined across segments for the selected packet, when its\n"
        "                  segment contributed to one. In-order segments only.\n"
        "  -h, --help      Show this help and exit\n"
        "\n"
        "Examples:\n"
        "  %s eth0\n"
        "  %s eth0 -f \"tcp port 443\"\n"
        "  %s -r out.pcapng\n",
        argv0, argv0, argv0, argv0);
}

}  // namespace

int main(int argc, char** argv) {
    for (int i = 1; i < argc; ++i) {
        if (std::strcmp(argv[i], "-h") == 0 || std::strcmp(argv[i], "--help") == 0) {
            print_usage(argv[0]);
            return 0;
        }
    }

    wireframe::CaptureSessionOptions options;
    bool enable_checksums = false;
    bool enable_reassembly = false;
    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 (std::strcmp(argv[i], "-c") == 0) {
            enable_checksums = true;
        } else if (std::strcmp(argv[i], "-a") == 0) {
            enable_reassembly = true;
        } 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;
    wireframe::net::TcpReassembler reassembler;
    std::thread capture_thread = session.start_capture_thread(queue);
    std::thread consumer_thread(consumer_loop, std::ref(session), std::ref(queue),
                                 std::ref(state), enable_reassembly ? &reassembler : nullptr);

    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())) {
                const auto& row = state.rows[selected_row];
                if (enable_checksums) {
                    std::string status = wireframe::checksum_status(row.data, session.datalink());
                    if (!status.empty()) {
                        ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s", status.c_str());
                    }
                }
                if (row.reassembled_http) {
                    ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "%s",
                                        row.reassembled_http->c_str());
                }
                for (const auto& line : wireframe::hex_dump_lines(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;
}