srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_checksum.cpp136
-rw-r--r--tests/test_icmp.cpp85
-rw-r--r--tests/test_privileges.cpp18
-rw-r--r--tests/test_tcp_reassembly.cpp176
4 files changed, 415 insertions, 0 deletions
diff --git a/tests/test_checksum.cpp b/tests/test_checksum.cpp
new file mode 100644
index 0000000..1295499
--- /dev/null
+++ b/tests/test_checksum.cpp
@@ -0,0 +1,136 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "wireframe/net/checksum.hpp"
+
+using namespace wireframe::net;
+
+namespace {
+
+// Mirrors checksum.hpp's own detail::build_ipv4_pseudo_header, kept
+// separate here deliberately: constructing expected test vectors using
+// the exact same private helper the code under test uses would make
+// these tests circular. A few duplicated lines of test-only setup is
+// the honest cost of testing independently.
+std::vector<unsigned char> pseudo_header(const std::array<unsigned char, 4>& src,
+ const std::array<unsigned char, 4>& dst,
+ unsigned char protocol,
+ std::span<const unsigned char> segment) {
+ std::vector<unsigned char> buf;
+ buf.insert(buf.end(), src.begin(), src.end());
+ buf.insert(buf.end(), dst.begin(), dst.end());
+ buf.push_back(0);
+ buf.push_back(protocol);
+ std::uint16_t len = static_cast<std::uint16_t>(segment.size());
+ buf.push_back(static_cast<unsigned char>(len >> 8));
+ buf.push_back(static_cast<unsigned char>(len & 0xFF));
+ buf.insert(buf.end(), segment.begin(), segment.end());
+ return buf;
+}
+
+} // namespace
+
+TEST_CASE("internet_checksum matches RFC 1071's own worked example") {
+ // The RFC's example data (0001 f203 f4f5 f6f7) computes to checksum
+ // 220d - an external reference, not derived from this code.
+ std::vector<unsigned char> data = {0x00, 0x01, 0xf2, 0x03, 0xf4, 0xf5, 0xf6, 0xf7};
+ CHECK(internet_checksum(data) == 0x220d);
+}
+
+TEST_CASE("internet_checksum of data with its own valid checksum appended is zero") {
+ // Direct consequence of the RFC 1071 example: appending that
+ // checksum as one more word should sum to all-ones, complementing
+ // to exactly zero - this is the actual verification technique
+ // verify_ipv4_checksum() etc. rely on.
+ std::vector<unsigned char> data = {0x00, 0x01, 0xf2, 0x03, 0xf4, 0xf5, 0xf6, 0xf7, 0x22, 0x0d};
+ CHECK(internet_checksum(data) == 0);
+}
+
+TEST_CASE("internet_checksum handles an odd-length buffer (trailing byte padded high)") {
+ std::vector<unsigned char> data = {0x00, 0x01, 0xf2}; // 3 bytes: one word + one odd byte
+ // 0x0001 + 0xf200 (odd byte in the high half) = 0xf201; ~0xf201 = 0x0dfe
+ CHECK(internet_checksum(data) == 0x0dfe);
+}
+
+TEST_CASE("verify_ipv4_checksum accepts a header with a correctly computed checksum") {
+ std::vector<unsigned char> header(20, 0);
+ header[0] = 0x45;
+ header[8] = 64; // ttl
+ header[9] = kProtoTcp;
+ header[12] = 10; header[13] = 0; header[14] = 0; header[15] = 1;
+ header[16] = 10; header[17] = 0; header[18] = 0; header[19] = 2;
+ // checksum field (bytes 10-11) computed with itself still zeroed
+ std::uint16_t csum = internet_checksum(header);
+ header[10] = static_cast<unsigned char>(csum >> 8);
+ header[11] = static_cast<unsigned char>(csum & 0xFF);
+
+ CHECK(verify_ipv4_checksum(header));
+}
+
+TEST_CASE("verify_ipv4_checksum rejects a header corrupted after the checksum was computed") {
+ std::vector<unsigned char> header(20, 0);
+ header[0] = 0x45;
+ header[9] = kProtoTcp;
+ std::uint16_t csum = internet_checksum(header);
+ header[10] = static_cast<unsigned char>(csum >> 8);
+ header[11] = static_cast<unsigned char>(csum & 0xFF);
+
+ header[15] ^= 0xFF; // flip a source-address byte after the fact
+ CHECK_FALSE(verify_ipv4_checksum(header));
+}
+
+TEST_CASE("verify_tcp_checksum_ipv4 accepts a segment with a correctly computed checksum") {
+ std::array<unsigned char, 4> src = {10, 0, 0, 1};
+ std::array<unsigned char, 4> dst = {10, 0, 0, 2};
+
+ std::vector<unsigned char> tcp(20, 0);
+ tcp[0] = 0; tcp[1] = 80; // src port
+ tcp[2] = 0x01; tcp[3] = 0xbb; // dst port 443
+ tcp[12] = 5 << 4; // data_offset = 5
+
+ auto buf = pseudo_header(src, dst, kProtoTcp, tcp);
+ std::uint16_t csum = internet_checksum(buf);
+ tcp[16] = static_cast<unsigned char>(csum >> 8);
+ tcp[17] = static_cast<unsigned char>(csum & 0xFF);
+
+ CHECK(verify_tcp_checksum_ipv4({src}, {dst}, tcp) == ChecksumResult::kValid);
+}
+
+TEST_CASE("verify_tcp_checksum_ipv4 rejects a segment corrupted after the checksum was computed") {
+ std::array<unsigned char, 4> src = {10, 0, 0, 1};
+ std::array<unsigned char, 4> dst = {10, 0, 0, 2};
+
+ std::vector<unsigned char> tcp(20, 0);
+ tcp[12] = 5 << 4;
+ auto buf = pseudo_header(src, dst, kProtoTcp, tcp);
+ std::uint16_t csum = internet_checksum(buf);
+ tcp[16] = static_cast<unsigned char>(csum >> 8);
+ tcp[17] = static_cast<unsigned char>(csum & 0xFF);
+
+ tcp[0] ^= 0xFF; // corrupt the source port after the fact
+ CHECK(verify_tcp_checksum_ipv4({src}, {dst}, tcp) == ChecksumResult::kInvalid);
+}
+
+TEST_CASE("verify_udp_checksum_ipv4 treats a transmitted checksum of 0x0000 as not present") {
+ std::array<unsigned char, 4> src = {10, 0, 0, 1};
+ std::array<unsigned char, 4> dst = {10, 0, 0, 2};
+ std::vector<unsigned char> udp = {0x00, 0x35, 0x00, 0x35, 0x00, 0x08, 0x00, 0x00}; // csum=0
+ CHECK(verify_udp_checksum_ipv4({src}, {dst}, udp) == ChecksumResult::kNotPresent);
+}
+
+TEST_CASE("verify_udp_checksum_ipv4 accepts a datagram with a correctly computed checksum") {
+ std::array<unsigned char, 4> src = {10, 0, 0, 1};
+ std::array<unsigned char, 4> dst = {10, 0, 0, 2};
+
+ std::vector<unsigned char> udp = {0x00, 0x35, 0x00, 0x35, 0x00, 0x08, 0x00, 0x00};
+ auto buf = pseudo_header(src, dst, kProtoUdp, udp);
+ std::uint16_t csum = internet_checksum(buf);
+ // A computed checksum of exactly 0 is itself sent as 0xFFFF per
+ // RFC 768, to keep it distinguishable from "no checksum" - not
+ // exercised by this test's specific values, but worth the note.
+ udp[6] = static_cast<unsigned char>(csum >> 8);
+ udp[7] = static_cast<unsigned char>(csum & 0xFF);
+
+ CHECK(verify_udp_checksum_ipv4({src}, {dst}, udp) == ChecksumResult::kValid);
+}
diff --git a/tests/test_icmp.cpp b/tests/test_icmp.cpp
new file mode 100644
index 0000000..3dd713e
--- /dev/null
+++ b/tests/test_icmp.cpp
@@ -0,0 +1,85 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "wireframe/net/icmp.hpp"
+
+using namespace wireframe::net;
+
+TEST_CASE("parse_icmpv4 decodes an echo request with identifier/sequence") {
+ std::vector<unsigned char> bytes = {8, 0, 0x00, 0x00, 0x1c, 0x05, 0x00, 0x01};
+ auto icmp = parse_icmpv4(bytes);
+ REQUIRE(icmp.has_value());
+ CHECK(icmp->type == 8);
+ CHECK(icmp->code == 0);
+ REQUIRE(icmp->identifier.has_value());
+ CHECK(*icmp->identifier == 0x1c05);
+ REQUIRE(icmp->sequence.has_value());
+ CHECK(*icmp->sequence == 1);
+}
+
+TEST_CASE("parse_icmpv4 decodes an echo reply the same way as a request") {
+ std::vector<unsigned char> bytes = {0, 0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02};
+ auto icmp = parse_icmpv4(bytes);
+ REQUIRE(icmp.has_value());
+ CHECK(icmp->type == 0);
+ REQUIRE(icmp->identifier.has_value());
+ CHECK(*icmp->identifier == 1);
+}
+
+TEST_CASE("parse_icmpv4 decodes a non-echo type without an identifier/sequence") {
+ std::vector<unsigned char> bytes = {3, 1, 0x00, 0x00}; // dest unreachable, host unreachable
+ auto icmp = parse_icmpv4(bytes);
+ REQUIRE(icmp.has_value());
+ CHECK(icmp->type == 3);
+ CHECK(icmp->code == 1);
+ CHECK_FALSE(icmp->identifier.has_value());
+}
+
+TEST_CASE("parse_icmpv4 rejects a buffer shorter than the fixed header") {
+ std::vector<unsigned char> bytes(3, 0);
+ CHECK_FALSE(parse_icmpv4(bytes).has_value());
+}
+
+TEST_CASE("icmpv4_type_name covers known types and falls back for unknown ones") {
+ CHECK(icmpv4_type_name(8) == "Echo Request");
+ CHECK(icmpv4_type_name(0) == "Echo Reply");
+ CHECK(icmpv4_type_name(3) == "Destination Unreachable");
+ CHECK(icmpv4_type_name(200) == "type=200");
+}
+
+TEST_CASE("parse_icmpv6 decodes an echo request with identifier/sequence") {
+ std::vector<unsigned char> bytes = {128, 0, 0x00, 0x00, 0x1c, 0x05, 0x00, 0x01};
+ auto icmp = parse_icmpv6(bytes);
+ REQUIRE(icmp.has_value());
+ CHECK(icmp->type == 128);
+ REQUIRE(icmp->identifier.has_value());
+ CHECK(*icmp->identifier == 0x1c05);
+}
+
+TEST_CASE("parse_icmpv6 decodes a non-echo type (e.g. Neighbor Solicitation) without id/seq") {
+ std::vector<unsigned char> bytes = {135, 0, 0x00, 0x00};
+ auto icmp = parse_icmpv6(bytes);
+ REQUIRE(icmp.has_value());
+ CHECK(icmp->type == 135);
+ CHECK_FALSE(icmp->identifier.has_value());
+}
+
+TEST_CASE("icmpv6_type_name covers known types and falls back for unknown ones") {
+ CHECK(icmpv6_type_name(128) == "Echo Request");
+ CHECK(icmpv6_type_name(135) == "Neighbor Solicitation");
+ CHECK(icmpv6_type_name(134) == "Router Advertisement");
+ CHECK(icmpv6_type_name(250) == "type=250");
+}
+
+TEST_CASE("the same type number means something different in each protocol's table") {
+ // The whole reason these are two separate tables, not one shared by
+ // number: ICMPv4's echo request is type 8, but ICMPv6's type 8
+ // isn't in its table at all (echo request is 128 there instead).
+ CHECK(icmpv4_type_name(8) == "Echo Request");
+ CHECK(icmpv6_type_name(8) == "type=8");
+ // And type 4 means "Parameter Problem" in ICMPv6 but is unmapped
+ // (falls back) in the ICMPv4 table.
+ CHECK(icmpv6_type_name(4) == "Parameter Problem");
+ CHECK(icmpv4_type_name(4) == "Source Quench");
+}
diff --git a/tests/test_privileges.cpp b/tests/test_privileges.cpp
new file mode 100644
index 0000000..287d654
--- /dev/null
+++ b/tests/test_privileges.cpp
@@ -0,0 +1,18 @@
+#include <doctest/doctest.h>
+#include <unistd.h>
+
+#include "wireframe/privileges.hpp"
+
+// The actual drop sequence (setuid/setgid) can only be meaningfully
+// exercised by literally running as root, which a unit test shouldn't
+// do - permanently dropping the test runner's own privileges mid-suite
+// would be a real, surprising side effect, not a safe thing to assert
+// on. That path is verified live instead (running the real binary via
+// sudo and checking the dropped-to UID actually took effect - see
+// PLAN.md). This only covers the no-op path any non-root test run
+// takes, which is still worth locking in: it must never attempt to
+// touch privileges it doesn't have.
+TEST_CASE("drop_privileges_if_root is a no-op when not running as root") {
+ if (geteuid() == 0) return; // this test only makes sense unprivileged
+ CHECK_FALSE(wireframe::drop_privileges_if_root().has_value());
+}
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 <doctest/doctest.h>
+
+#include <string>
+#include <vector>
+
+#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<unsigned char> to_bytes(const std::string& s) {
+ return std::vector<unsigned char>(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<std::uint32_t>(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);
+}