srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-06-25 22:11:00 +0200
committersrdusr <[email protected]>2025-06-25 22:11:00 +0200
commitd9bedcec1bce8d15de6403377702d51d1bcb862f (patch)
tree363621175b93fa347dc288f9e53a8ede2d453d6d /tests
parentf8fc8806401596b08779cf8c88da31408c9b0547 (diff)
downloadpacketeer-d9bedcec1bce8d15de6403377702d51d1bcb862f.tar.gz
packeteer-d9bedcec1bce8d15de6403377702d51d1bcb862f.zip
Add NTP and DHCP L7 dissectors
NTP decodes the fixed header's version/mode/stratum - the timestamp fields need NTP era/fraction fixed-point math to render meaningfully and add nothing a one-line summary needs, so they're left alone. DHCP decodes RFC 2131's BOOTP fixed header + magic cookie, then walks the TLV options bounds-safely for option 53 (message type), plus yiaddr when the server has assigned one. It's the first dissector needing two well-known ports (67 server, 68 client) rather than one; registering at just 67 still matches both directions since l7_summarize() already falls back from dst_port to src_port. Verified live: NTP against a real pool.ntp.org query on wlp1s0 (a genuine stratum-2 reply came back). DHCP over loopback with a synthetic-but-wire-format-real DISCOVER/OFFER exchange rather than a real lease renewal, to avoid disrupting this machine's actual network state - caught a test-setup mistake in the process (both packets sent from the same ephemeral port instead of the OFFER actually originating from port 67), not a dissector bug.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_dhcp.cpp111
-rw-r--r--tests/test_ntp.cpp64
2 files changed, 175 insertions, 0 deletions
diff --git a/tests/test_dhcp.cpp b/tests/test_dhcp.cpp
new file mode 100644
index 0000000..e9cbc16
--- /dev/null
+++ b/tests/test_dhcp.cpp
@@ -0,0 +1,111 @@
+#include <doctest/doctest.h>
+
+#include <algorithm>
+#include <array>
+#include <vector>
+
+#include "packeteer/l7/dhcp.hpp"
+
+using namespace packeteer::net;
+
+namespace {
+
+// Builds a minimal BOOTP fixed header + magic cookie + a message-type
+// option (53), optionally with yiaddr set.
+std::vector<unsigned char> dhcp_message(std::uint8_t op, std::uint8_t message_type,
+ std::array<unsigned char, 4> yiaddr = {0, 0, 0, 0}) {
+ std::vector<unsigned char> bytes(240, 0);
+ bytes[0] = op;
+ std::copy(yiaddr.begin(), yiaddr.end(), bytes.begin() + 16);
+ bytes[236] = 0x63;
+ bytes[237] = 0x82;
+ bytes[238] = 0x53;
+ bytes[239] = 0x63;
+
+ // Option 53 (message type), length 1, then End.
+ bytes.push_back(53);
+ bytes.push_back(1);
+ bytes.push_back(message_type);
+ bytes.push_back(0xFF);
+ return bytes;
+}
+
+} // namespace
+
+TEST_CASE("parse_dhcp decodes a DISCOVER") {
+ auto msg = parse_dhcp(dhcp_message(1, kDhcpDiscover));
+ REQUIRE(msg.has_value());
+ CHECK(msg->op == 1);
+ REQUIRE(msg->message_type.has_value());
+ CHECK(*msg->message_type == kDhcpDiscover);
+}
+
+TEST_CASE("parse_dhcp decodes an OFFER with yiaddr set") {
+ auto msg = parse_dhcp(dhcp_message(2, kDhcpOffer, {192, 168, 1, 50}));
+ REQUIRE(msg.has_value());
+ REQUIRE(msg->message_type.has_value());
+ CHECK(*msg->message_type == kDhcpOffer);
+ CHECK(msg->yiaddr.bytes == std::array<unsigned char, 4>{192, 168, 1, 50});
+}
+
+TEST_CASE("parse_dhcp rejects a payload without the magic cookie") {
+ std::vector<unsigned char> bytes(240, 0); // right size, but cookie bytes are zero
+ CHECK_FALSE(parse_dhcp(bytes).has_value());
+}
+
+TEST_CASE("parse_dhcp rejects a payload shorter than the fixed header + cookie") {
+ std::vector<unsigned char> bytes(100, 0);
+ CHECK_FALSE(parse_dhcp(bytes).has_value());
+}
+
+TEST_CASE("parse_dhcp handles a message with no options gracefully") {
+ std::vector<unsigned char> bytes(240, 0);
+ bytes[0] = 1;
+ bytes[236] = 0x63; bytes[237] = 0x82; bytes[238] = 0x53; bytes[239] = 0x63;
+ auto msg = parse_dhcp(bytes);
+ REQUIRE(msg.has_value());
+ CHECK_FALSE(msg->message_type.has_value());
+}
+
+TEST_CASE("parse_dhcp skips pad options while scanning for message type") {
+ std::vector<unsigned char> bytes(240, 0);
+ bytes[236] = 0x63; bytes[237] = 0x82; bytes[238] = 0x53; bytes[239] = 0x63;
+ bytes.push_back(0x00); // pad
+ bytes.push_back(0x00); // pad
+ bytes.push_back(53);
+ bytes.push_back(1);
+ bytes.push_back(kDhcpAck);
+ bytes.push_back(0xFF);
+
+ auto msg = parse_dhcp(bytes);
+ REQUIRE(msg.has_value());
+ REQUIRE(msg->message_type.has_value());
+ CHECK(*msg->message_type == kDhcpAck);
+}
+
+TEST_CASE("DhcpDissector claims port 67 and names message types") {
+ DhcpDissector dissector;
+ CHECK(dissector.port() == kDhcpServerPort);
+
+ auto summary = dissector.summarize(dhcp_message(2, kDhcpOffer, {192, 168, 1, 50}));
+ REQUIRE(summary.has_value());
+ CHECK(*summary == "DHCP OFFER yiaddr=192.168.1.50");
+}
+
+TEST_CASE("DhcpDissector omits yiaddr when it's all-zero") {
+ DhcpDissector dissector;
+ auto summary = dissector.summarize(dhcp_message(1, kDhcpDiscover));
+ REQUIRE(summary.has_value());
+ CHECK(*summary == "DHCP DISCOVER");
+}
+
+TEST_CASE("DhcpDissector falls back to op when message type is absent") {
+ DhcpDissector dissector;
+ std::vector<unsigned char> bytes(240, 0);
+ bytes[0] = 1;
+ bytes[236] = 0x63; bytes[237] = 0x82; bytes[238] = 0x53; bytes[239] = 0x63;
+
+ auto summary = dissector.summarize(bytes);
+ REQUIRE(summary.has_value());
+ CHECK(*summary == "DHCP request");
+}
diff --git a/tests/test_ntp.cpp b/tests/test_ntp.cpp
new file mode 100644
index 0000000..2c93302
--- /dev/null
+++ b/tests/test_ntp.cpp
@@ -0,0 +1,64 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "packeteer/l7/ntp.hpp"
+
+using namespace packeteer::net;
+
+namespace {
+
+std::vector<unsigned char> ntp_client_request() {
+ std::vector<unsigned char> bytes(48, 0);
+ bytes[0] = (4 << 3) | 3; // version 4, mode 3 (client)
+ bytes[1] = 0; // stratum: not applicable on a client request
+ return bytes;
+}
+
+std::vector<unsigned char> ntp_server_reply() {
+ std::vector<unsigned char> bytes(48, 0);
+ bytes[0] = (4 << 3) | 4; // version 4, mode 4 (server)
+ bytes[1] = 2; // stratum 2
+ return bytes;
+}
+
+} // namespace
+
+TEST_CASE("parse_ntp decodes a client request") {
+ auto ntp = parse_ntp(ntp_client_request());
+ REQUIRE(ntp.has_value());
+ CHECK(ntp->version == 4);
+ CHECK(ntp->mode == 3);
+}
+
+TEST_CASE("parse_ntp decodes a server reply with its stratum") {
+ auto ntp = parse_ntp(ntp_server_reply());
+ REQUIRE(ntp.has_value());
+ CHECK(ntp->mode == 4);
+ CHECK(ntp->stratum == 2);
+}
+
+TEST_CASE("parse_ntp rejects a payload shorter than the fixed header") {
+ std::vector<unsigned char> bytes(20, 0);
+ CHECK_FALSE(parse_ntp(bytes).has_value());
+}
+
+TEST_CASE("NtpDissector claims port 123 and includes stratum for client/server modes") {
+ NtpDissector dissector;
+ CHECK(dissector.port() == kNtpPort);
+
+ auto summary = dissector.summarize(ntp_server_reply());
+ REQUIRE(summary.has_value());
+ CHECK(*summary == "NTP v4 server stratum=2");
+}
+
+TEST_CASE("NtpDissector omits stratum for non-client/server modes") {
+ NtpDissector dissector;
+ std::vector<unsigned char> bytes(48, 0);
+ bytes[0] = (4 << 3) | 5; // mode 5: broadcast
+ bytes[1] = 1;
+
+ auto summary = dissector.summarize(bytes);
+ REQUIRE(summary.has_value());
+ CHECK(summary->find("stratum") == std::string::npos);
+}