srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/include/wireframe/summarize.hpp
blob: 59aa621fd1f3a5dea170681824100c518100002d (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
#pragma once

#include <cstdio>
#include <optional>
#include <span>
#include <string>
#include <vector>

#include <pcap.h>

#include "wireframe/l7/dissector.hpp"
#include "wireframe/l7/dns.hpp"
#include "wireframe/l7/http.hpp"
#include "wireframe/l7/tls.hpp"
#include "wireframe/net/ethernet.hpp"
#include "wireframe/net/ipv4.hpp"
#include "wireframe/net/ipv6.hpp"
#include "wireframe/net/tcp.hpp"
#include "wireframe/net/udp.hpp"

// Packet -> human-readable summary. Shared by every frontend (plain
// CLI, TUI, GUI) so they can't drift apart on what a given packet
// decodes to - one source of truth, not three copies to keep in sync.
namespace wireframe {

inline std::string mac_to_string(const net::MacAddress& mac) {
    char buf[18];
    std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", mac.bytes[0], mac.bytes[1],
                  mac.bytes[2], mac.bytes[3], mac.bytes[4], mac.bytes[5]);
    return buf;
}

inline std::string ipv4_to_string(const net::Ipv4Address& ip) {
    char buf[16];
    std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", ip.bytes[0], ip.bytes[1], ip.bytes[2],
                  ip.bytes[3]);
    return buf;
}

inline std::string tcp_flags_to_string(std::uint8_t flags) {
    using namespace net;
    std::string out;
    if (flags & kTcpSyn) out += 'S';
    if (flags & kTcpAck) out += 'A';
    if (flags & kTcpFin) out += 'F';
    if (flags & kTcpRst) out += 'R';
    if (flags & kTcpPsh) out += 'P';
    if (flags & kTcpUrg) out += 'U';
    return out.empty() ? "-" : out;
}

// Registered once. DNS (UDP) was the first L7 dissector, proving the
// interface (wireframe/l7/dissector.hpp) is enough to add a protocol
// without touching the L2-L4 decode path; HTTP (TCP) is the second,
// and the first to actually exercise L7Registry's TCP-payload path --
// DNS alone never did, since it only ever runs over UDP port 53. TLS
// (also TCP, port 443) covers what HTTP increasingly can't: most web
// traffic today is encrypted, and SNI is the one piece of a TLS
// handshake still readable without decrypting anything.
inline const net::L7Registry& l7_registry() {
    static const net::DnsDissector dns_dissector;
    static const net::HttpDissector http_dissector;
    static const net::TlsSniDissector tls_dissector;
    static const net::L7Registry registry = [] {
        net::L7Registry r;
        r.add(&dns_dissector);
        r.add(&http_dissector);
        r.add(&tls_dissector);
        return r;
    }();
    return registry;
}

// Tries the destination port first (the common case: a client talking
// to a well-known server port), then the source port (a server's
// reply, coming from that same well-known port).
inline std::optional<std::string> l7_summarize(std::span<const unsigned char> payload,
                                                 std::uint16_t src_port, std::uint16_t dst_port) {
    if (auto summary = l7_registry().dissect(dst_port, payload)) return summary;
    return l7_registry().dissect(src_port, payload);
}

// IPv4 and IPv6 headers carry different fields (ttl vs. hop_limit,
// 4-byte vs. 16-byte addresses), but everything above the IP layer --
// TCP/UDP decode plus the L7 lookup - is identical once normalized to
// this. Keeping that dispatch in one place means TCP/UDP/L7 formatting
// can't drift between the two IP versions.
struct IpInfo {
    const char* label;  // "IPv4" or "IPv6"
    std::string src_str;
    std::string dst_str;
    std::uint8_t ttl_or_hop_limit;
    std::uint8_t proto;
    std::span<const unsigned char> payload;
};

inline std::string summarize_transport_and_above(const IpInfo& info) {
    char ip_buf[160];
    std::snprintf(ip_buf, sizeof(ip_buf), " | %s %s -> %s ttl=%u proto=%u", info.label,
                  info.src_str.c_str(), info.dst_str.c_str(), info.ttl_or_hop_limit, info.proto);
    std::string out = ip_buf;

    if (info.proto == net::kProtoTcp) {
        if (auto tcp = net::parse_tcp(info.payload)) {
            char tcp_buf[128];
            std::snprintf(tcp_buf, sizeof(tcp_buf), " | TCP %u -> %u [%s] seq=%u ack=%u win=%u",
                          tcp->header.src_port, tcp->header.dst_port,
                          tcp_flags_to_string(tcp->header.flags).c_str(), tcp->header.seq,
                          tcp->header.ack, tcp->header.window);
            out += tcp_buf;
            if (auto l7 = l7_summarize(tcp->payload, tcp->header.src_port, tcp->header.dst_port)) {
                out += " | " + *l7;
            }
        }
    } else if (info.proto == net::kProtoUdp) {
        if (auto udp = net::parse_udp(info.payload)) {
            char udp_buf[64];
            std::snprintf(udp_buf, sizeof(udp_buf), " | UDP %u -> %u len=%u",
                          udp->header.src_port, udp->header.dst_port, udp->header.length);
            out += udp_buf;
            if (auto l7 = l7_summarize(udp->payload, udp->header.src_port, udp->header.dst_port)) {
                out += " | " + *l7;
            }
        }
    } else if (info.proto == net::kNextHeaderIcmpv6) {
        out += " | ICMPv6";
    }
    return out;
}

// `datalink` is the interface's actual pcap_datalink() type, not an
// assumption: tunnel/VPN interfaces (tailscale0, wireguard, plain
// tun/tap) hand libpcap raw IP with no link-layer header at all
// (DLT_RAW), unlike a real NIC or even `lo` (both DLT_EN10MB on
// Linux). Treating raw IP bytes as an Ethernet frame silently produces
// garbage MACs and ethertypes - verified by actually capturing on
// tailscale0 before this branch existed.
//
// IP version is read from the packet itself (the first nibble), not
// inferred from ethertype/datalink: DLT_RAW has no ethertype to key
// off at all, and even on Ethernet this keeps IPv4/IPv6 dispatch in
// one place.
inline std::string summarize_packet(std::span<const unsigned char> bytes, int datalink) {
    std::span<const unsigned char> ip_bytes;
    std::string out;

    if (datalink == DLT_RAW) {
        out = "RAW";
        ip_bytes = bytes;
    } else {
        auto eth = net::parse_ethernet(bytes);
        if (!eth) {
            char buf[64];
            std::snprintf(buf, sizeof(buf), "[%zu bytes] truncated ethernet frame", bytes.size());
            return buf;
        }

        out = "ETH " + mac_to_string(eth->header.src) + " -> " + mac_to_string(eth->header.dst);
        char eth_buf[32];
        std::snprintf(eth_buf, sizeof(eth_buf), " ethertype=0x%04x", eth->header.ethertype);
        out += eth_buf;

        if (eth->header.ethertype != net::kEthertypeIPv4 &&
            eth->header.ethertype != net::kEthertypeIPv6) {
            return out;
        }
        ip_bytes = eth->payload;
    }

    if (ip_bytes.empty()) {
        out += " | IP (empty payload)";
        return out;
    }
    std::uint8_t version = static_cast<std::uint8_t>(ip_bytes[0] >> 4);

    if (version == 4) {
        auto ip = net::parse_ipv4(ip_bytes);
        if (!ip) {
            out += " | IPv4 (truncated)";
            return out;
        }
        out += summarize_transport_and_above({"IPv4", ipv4_to_string(ip->header.src),
                                               ipv4_to_string(ip->header.dst), ip->header.ttl,
                                               ip->header.protocol, ip->payload});
    } else if (version == 6) {
        auto ip6 = net::parse_ipv6(ip_bytes);
        if (!ip6) {
            out += " | IPv6 (truncated)";
            return out;
        }
        std::string src_str = net::ipv6_to_string(ip6->header.src);
        std::string dst_str = net::ipv6_to_string(ip6->header.dst);

        // next_header may name an extension header (Hop-by-Hop,
        // Routing, Dest Options, Fragment, AH) rather than the actual
        // transport protocol; walk through those to find it.
        auto walked = net::walk_ipv6_extension_headers(ip6->header.next_header, ip6->payload);
        if (walked.stopped_at_esp) {
            char buf[160];
            std::snprintf(buf, sizeof(buf), " | IPv6 %s -> %s ttl=%u proto=%u | ESP (encrypted)",
                          src_str.c_str(), dst_str.c_str(), ip6->header.hop_limit,
                          net::kNextHeaderEsp);
            out += buf;
        } else {
            out += summarize_transport_and_above({"IPv6", src_str, dst_str, ip6->header.hop_limit,
                                                   walked.final_next_header, walked.payload});
        }
    } else {
        out += " | IP version " + std::to_string(version) + " (unsupported)";
    }
    return out;
}

// One formatted line per 16 bytes: offset, hex, ASCII gutter. Returned
// as lines rather than printed so both the CLI's -x output and a GUI
// details pane can use the same formatting.
inline std::vector<std::string> hex_dump_lines(std::span<const unsigned char> bytes) {
    std::vector<std::string> lines;
    for (std::size_t offset = 0; offset < bytes.size(); offset += 16) {
        char offset_buf[32];
        std::snprintf(offset_buf, sizeof(offset_buf), "%06zx  ", offset);
        std::string line = offset_buf;

        std::size_t line_len = std::min<std::size_t>(16, bytes.size() - offset);
        for (std::size_t i = 0; i < 16; ++i) {
            if (i < line_len) {
                char byte_buf[4];
                std::snprintf(byte_buf, sizeof(byte_buf), "%02x ", bytes[offset + i]);
                line += byte_buf;
            } else {
                line += "   ";
            }
            if (i == 7) line += ' ';
        }

        line += " |";
        for (std::size_t i = 0; i < line_len; ++i) {
            unsigned char c = bytes[offset + i];
            line += (c >= 0x20 && c < 0x7f) ? static_cast<char>(c) : '.';
        }
        line += '|';

        lines.push_back(std::move(line));
    }
    return lines;
}

}  // namespace wireframe