From e0f4c701028aa81026a17cf9ebfb36112184f4bc Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Fri, 17 May 2024 19:54:00 +0200 Subject: Add privilege dropping, AF_PACKET demo, ICMP, checksum validation, --help, and TCP reassembly Rounds out the build order in PLAN.md with six incremental additions: drop root privileges immediately after opening the capture handle; a standalone AF_PACKET/mmap ring-buffer demo (kept separate from CaptureSession, see its header comment for why); ICMPv4/ICMPv6 type and code decoding; opt-in IPv4/TCP/UDP checksum validation (-c); CLI --help; and opt-in, in-order-only TCP stream reassembly (-a) so HTTP requests/responses split across segments can be seen whole. Each addition is unit-tested and, where it touches live traffic behavior, verified against real captured packets - see PLAN.md's Decisions section for the verification notes on each. --- tests/test_tcp_reassembly.cpp | 176 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 tests/test_tcp_reassembly.cpp (limited to 'tests/test_tcp_reassembly.cpp') diff --git a/tests/test_tcp_reassembly.cpp b/tests/test_tcp_reassembly.cpp new file mode 100644 index 0000000..b424610 --- /dev/null +++ b/tests/test_tcp_reassembly.cpp @@ -0,0 +1,176 @@ +#include + +#include +#include + +#include "wireframe/l7/http.hpp" +#include "wireframe/net/tcp.hpp" +#include "wireframe/net/tcp_reassembly.hpp" + +using namespace wireframe::net; + +namespace { + +Ipv4Address addr(unsigned char a, unsigned char b, unsigned char c, unsigned char d) { + return Ipv4Address{{a, b, c, d}}; +} + +std::vector to_bytes(const std::string& s) { + return std::vector(s.begin(), s.end()); +} + +} // namespace + +TEST_CASE("TcpReassembler ignores payload before SYN is seen") { + TcpReassembler r; + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + auto data = to_bytes("data before syn"); + auto result = r.process_segment(client, 40000, server, 80, 1000, 0, data); + CHECK_FALSE(result.has_value()); +} + +TEST_CASE("TcpReassembler joins two in-order segments into one contiguous buffer") { + TcpReassembler r; + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + // SYN: seq 1000, consumes seq 1000 itself, next data starts at 1001. + auto syn = r.process_segment(client, 40000, server, 80, 1000, kTcpSyn, {}); + CHECK_FALSE(syn.has_value()); + + auto part1 = to_bytes("GET /index.html HTTP/1.1\r\n"); + auto r1 = r.process_segment(client, 40000, server, 80, 1001, kTcpPsh | kTcpAck, part1); + REQUIRE(r1.has_value()); + CHECK(r1->size() == part1.size()); + + auto part2 = to_bytes("Host: example.com\r\n\r\n"); + std::uint32_t seq2 = 1001 + static_cast(part1.size()); + auto r2 = r.process_segment(client, 40000, server, 80, seq2, kTcpPsh | kTcpAck, part2); + REQUIRE(r2.has_value()); + + std::string joined(r2->begin(), r2->end()); + CHECK(joined == "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"); + + auto http = parse_http(*r2); + REQUIRE(http.has_value()); + CHECK(http->is_request); + CHECK(http->method_or_version == "GET"); + CHECK(http->target_or_status == "/index.html"); + REQUIRE(http->host.has_value()); + CHECK(*http->host == "example.com"); +} + +TEST_CASE("TcpReassembler drops an out-of-order segment rather than buffering it") { + TcpReassembler r; + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + r.process_segment(client, 40000, server, 80, 1000, kTcpSyn, {}); + + auto part1 = to_bytes("first "); + r.process_segment(client, 40000, server, 80, 1001, kTcpAck, part1); + + // Skip ahead instead of continuing at 1001 + part1.size(): out of order. + auto part3 = to_bytes("third "); + auto result = r.process_segment(client, 40000, server, 80, 9999, kTcpAck, part3); + CHECK_FALSE(result.has_value()); +} + +TEST_CASE("TcpReassembler drops a retransmitted (already-seen) segment") { + TcpReassembler r; + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + r.process_segment(client, 40000, server, 80, 1000, kTcpSyn, {}); + + auto part1 = to_bytes("hello"); + auto r1 = r.process_segment(client, 40000, server, 80, 1001, kTcpAck, part1); + REQUIRE(r1.has_value()); + + // Same seq again: a retransmission, not new data. + auto retransmit = r.process_segment(client, 40000, server, 80, 1001, kTcpAck, part1); + CHECK_FALSE(retransmit.has_value()); +} + +TEST_CASE("TcpReassembler tracks each direction of a flow independently") { + TcpReassembler r; + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + r.process_segment(client, 40000, server, 80, 1000, kTcpSyn, {}); + r.process_segment(server, 80, client, 40000, 5000, kTcpSyn | kTcpAck, {}); + + auto request = to_bytes("GET / HTTP/1.1\r\n\r\n"); + auto req_result = r.process_segment(client, 40000, server, 80, 1001, kTcpPsh | kTcpAck, + request); + REQUIRE(req_result.has_value()); + CHECK(std::string(req_result->begin(), req_result->end()) == "GET / HTTP/1.1\r\n\r\n"); + + auto response = to_bytes("HTTP/1.1 200 OK\r\n\r\n"); + auto resp_result = r.process_segment(server, 80, client, 40000, 5001, kTcpPsh | kTcpAck, + response); + REQUIRE(resp_result.has_value()); + CHECK(std::string(resp_result->begin(), resp_result->end()) == "HTTP/1.1 200 OK\r\n\r\n"); + + // Requesting side's buffer should be untouched by the response. + CHECK(std::string(req_result->begin(), req_result->end()) == "GET / HTTP/1.1\r\n\r\n"); +} + +TEST_CASE("TcpReassembler canonicalizes both directions of a connection to the same flow") { + TcpReassembler r; + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + r.process_segment(client, 40000, server, 80, 1000, kTcpSyn, {}); + CHECK(r.flow_count() == 1); + + // A segment in the reverse direction of the *same* connection must + // not create a second flow entry. + r.process_segment(server, 80, client, 40000, 5000, kTcpSyn | kTcpAck, {}); + CHECK(r.flow_count() == 1); +} + +TEST_CASE("TcpReassembler caps buffered bytes per direction and stops growing past the limit") { + TcpReassembler r(/*max_buffer_per_direction=*/10, /*max_flows=*/16); + auto client = addr(10, 0, 0, 1); + auto server = addr(10, 0, 0, 2); + + r.process_segment(client, 40000, server, 80, 1000, kTcpSyn, {}); + + auto part1 = to_bytes("12345"); // 5 bytes, fits + auto r1 = r.process_segment(client, 40000, server, 80, 1001, kTcpAck, part1); + REQUIRE(r1.has_value()); + CHECK(r1->size() == 5); + + // Next 5 bytes would land exactly at the 10-byte cap. + auto part2 = to_bytes("67890"); + auto r2 = r.process_segment(client, 40000, server, 80, 1006, kTcpAck, part2); + REQUIRE(r2.has_value()); + CHECK(r2->size() == 10); + + // A further segment would exceed the cap: sequence tracking still + // advances (so future in-order segments aren't misjudged), but the + // buffer itself does not grow past max_buffer_. + auto part3 = to_bytes("overflow"); + auto r3 = r.process_segment(client, 40000, server, 80, 1011, kTcpAck, part3); + REQUIRE(r3.has_value()); + CHECK(r3->size() == 10); +} + +TEST_CASE("TcpReassembler caps the number of tracked flows") { + TcpReassembler r(/*max_buffer_per_direction=*/1024, /*max_flows=*/1); + auto server = addr(10, 0, 0, 2); + + auto client1 = addr(10, 0, 0, 1); + r.process_segment(client1, 40000, server, 80, 1000, kTcpSyn, {}); + CHECK(r.flow_count() == 1); + + // A second, distinct flow should be refused: table is full. + auto client2 = addr(10, 0, 0, 3); + auto data = to_bytes("x"); + auto result = r.process_segment(client2, 40000, server, 80, 2000, kTcpSyn, data); + CHECK_FALSE(result.has_value()); + CHECK(r.flow_count() == 1); +} -- cgit v1.2.3