srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_ipv6.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_ipv6.cpp')
-rw-r--r--tests/test_ipv6.cpp169
1 files changed, 169 insertions, 0 deletions
diff --git a/tests/test_ipv6.cpp b/tests/test_ipv6.cpp
new file mode 100644
index 0000000..438fadc
--- /dev/null
+++ b/tests/test_ipv6.cpp
@@ -0,0 +1,169 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "wireframe/net/ipv4.hpp" // for kProtoTcp
+#include "wireframe/net/ipv6.hpp"
+
+using namespace wireframe::net;
+
+namespace {
+
+Ipv6Address addr_from_groups(std::array<std::uint16_t, 8> groups) {
+ Ipv6Address addr{};
+ for (std::size_t i = 0; i < 8; ++i) {
+ addr.bytes[i * 2] = static_cast<unsigned char>(groups[i] >> 8);
+ addr.bytes[i * 2 + 1] = static_cast<unsigned char>(groups[i] & 0xFF);
+ }
+ return addr;
+}
+
+} // namespace
+
+TEST_CASE("parse_ipv6 decodes header fields and leaves the right payload") {
+ std::vector<unsigned char> bytes(40, 0);
+ bytes[0] = 0x60; // version 6, traffic class high nibble 0
+ bytes[1] = 0x00; // traffic class low nibble 0, flow label starts 0
+ bytes[4] = 0x00;
+ bytes[5] = 0x04; // payload_length = 4
+ bytes[6] = kProtoTcp;
+ bytes[7] = 64; // hop_limit
+ // src = 2001:0db8::1
+ bytes[8] = 0x20; bytes[9] = 0x01; bytes[10] = 0x0d; bytes[11] = 0xb8;
+ bytes[23] = 0x01;
+ // dst = ::1
+ bytes[39] = 0x01;
+ bytes.insert(bytes.end(), {0xAA, 0xBB, 0xCC, 0xDD});
+
+ auto ip6 = parse_ipv6(bytes);
+ REQUIRE(ip6.has_value());
+ CHECK(ip6->header.version == 6);
+ CHECK(ip6->header.payload_length == 4);
+ CHECK(ip6->header.next_header == kProtoTcp);
+ CHECK(ip6->header.hop_limit == 64);
+ REQUIRE(ip6->payload.size() == 4);
+ CHECK(ip6->payload[0] == 0xAA);
+}
+
+TEST_CASE("parse_ipv6 rejects a non-IPv6 version") {
+ std::vector<unsigned char> bytes(40, 0);
+ bytes[0] = 0x45; // version 4
+ CHECK_FALSE(parse_ipv6(bytes).has_value());
+}
+
+TEST_CASE("parse_ipv6 rejects a buffer shorter than the 40-byte header") {
+ std::vector<unsigned char> bytes(39, 0);
+ bytes[0] = 0x60;
+ CHECK_FALSE(parse_ipv6(bytes).has_value());
+}
+
+TEST_CASE("ipv6_to_string compresses the loopback address") {
+ CHECK(ipv6_to_string(addr_from_groups({0, 0, 0, 0, 0, 0, 0, 1})) == "::1");
+}
+
+TEST_CASE("ipv6_to_string compresses the unspecified address") {
+ CHECK(ipv6_to_string(addr_from_groups({0, 0, 0, 0, 0, 0, 0, 0})) == "::");
+}
+
+TEST_CASE("ipv6_to_string compresses a zero run in the middle") {
+ CHECK(ipv6_to_string(addr_from_groups({0x2001, 0x0db8, 0, 0, 0, 0, 0, 1})) == "2001:db8::1");
+}
+
+TEST_CASE("ipv6_to_string does not compress a lone zero group") {
+ CHECK(ipv6_to_string(addr_from_groups({0x2001, 0, 0x0db8, 1, 1, 1, 1, 1})) ==
+ "2001:0:db8:1:1:1:1:1");
+}
+
+TEST_CASE("ipv6_to_string picks the leftmost run when two runs tie in length") {
+ // Two runs of length 2: groups[1..2] and groups[5..6]. Leftmost wins.
+ CHECK(ipv6_to_string(addr_from_groups({1, 0, 0, 2, 3, 0, 0, 4})) == "1::2:3:0:0:4");
+}
+
+TEST_CASE("ipv6_to_string leaves an address with no zero run untouched") {
+ CHECK(ipv6_to_string(addr_from_groups({1, 2, 3, 4, 5, 6, 7, 8})) == "1:2:3:4:5:6:7:8");
+}
+
+TEST_CASE("walk_ipv6_extension_headers passes a direct transport protocol through unchanged") {
+ std::vector<unsigned char> payload = {0xAA, 0xBB, 0xCC};
+ auto result = walk_ipv6_extension_headers(kProtoTcp, payload);
+ CHECK(result.final_next_header == kProtoTcp);
+ CHECK_FALSE(result.stopped_at_esp);
+ REQUIRE(result.payload.size() == 3);
+ CHECK(result.payload[0] == 0xAA);
+}
+
+TEST_CASE("walk_ipv6_extension_headers walks a single Hop-by-Hop header to reach TCP") {
+ // Hop-by-Hop: next_header(1)=TCP, hdr_ext_len(1)=0 -> total len (0+1)*8=8 bytes.
+ std::vector<unsigned char> payload = {static_cast<unsigned char>(kProtoTcp), 0x00,
+ 0, 0, 0, 0, 0, 0}; // 6 bytes of option padding
+ std::vector<unsigned char> tcp_marker = {0xDE, 0xAD};
+ payload.insert(payload.end(), tcp_marker.begin(), tcp_marker.end());
+
+ auto result = walk_ipv6_extension_headers(kNextHeaderHopByHop, payload);
+ CHECK(result.final_next_header == kProtoTcp);
+ CHECK_FALSE(result.stopped_at_esp);
+ REQUIRE(result.payload.size() == 2);
+ CHECK(result.payload[0] == 0xDE);
+}
+
+TEST_CASE("walk_ipv6_extension_headers walks a chain of two extension headers") {
+ // Hop-by-Hop (8 bytes) -> Destination Options (8 bytes) -> UDP.
+ std::vector<unsigned char> payload = {
+ kNextHeaderDestOptions, 0x00, 0, 0, 0, 0, 0, 0, // Hop-by-Hop, len 8
+ static_cast<unsigned char>(kProtoUdp), 0x00, 0, 0, 0, 0, 0, 0, // Dest Options, len 8
+ 0xFE, 0xED, // "UDP header" marker
+ };
+ auto result = walk_ipv6_extension_headers(kNextHeaderHopByHop, payload);
+ CHECK(result.final_next_header == kProtoUdp);
+ REQUIRE(result.payload.size() == 2);
+ CHECK(result.payload[0] == 0xFE);
+}
+
+TEST_CASE("walk_ipv6_extension_headers walks the fixed-size Fragment header") {
+ std::vector<unsigned char> payload = {static_cast<unsigned char>(kProtoTcp), 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 8-byte fragment header
+ 0xCA, 0xFE};
+ auto result = walk_ipv6_extension_headers(kNextHeaderFragment, payload);
+ CHECK(result.final_next_header == kProtoTcp);
+ REQUIRE(result.payload.size() == 2);
+ CHECK(result.payload[0] == 0xCA);
+}
+
+TEST_CASE("walk_ipv6_extension_headers applies AH's 4-byte-unit length formula") {
+ // AH: next_header(1)=TCP, payload_len(1)=1 -> total len (1+2)*4=12 bytes.
+ std::vector<unsigned char> payload(12, 0);
+ payload[0] = static_cast<unsigned char>(kProtoTcp);
+ payload[1] = 0x01;
+ payload.push_back(0x11);
+ payload.push_back(0x22);
+
+ auto result = walk_ipv6_extension_headers(kNextHeaderAh, payload);
+ CHECK(result.final_next_header == kProtoTcp);
+ REQUIRE(result.payload.size() == 2);
+ CHECK(result.payload[0] == 0x11);
+}
+
+TEST_CASE("walk_ipv6_extension_headers stops at ESP without guessing past it") {
+ std::vector<unsigned char> payload = {0x01, 0x02, 0x03, 0x04};
+ auto result = walk_ipv6_extension_headers(kNextHeaderEsp, payload);
+ CHECK(result.stopped_at_esp);
+ CHECK(result.final_next_header == kNextHeaderEsp);
+ REQUIRE(result.payload.size() == 4);
+ CHECK(result.payload[0] == 0x01); // untouched: ESP payload starts right here
+}
+
+TEST_CASE("walk_ipv6_extension_headers stops gracefully on a truncated extension header") {
+ std::vector<unsigned char> payload = {static_cast<unsigned char>(kProtoTcp),
+ 0xFF}; // claims (255+1)*8 bytes; nowhere near present
+ auto result = walk_ipv6_extension_headers(kNextHeaderHopByHop, payload);
+ CHECK(result.final_next_header == kNextHeaderHopByHop); // never resolved past it
+ CHECK_FALSE(result.stopped_at_esp);
+}
+
+TEST_CASE("walk_ipv6_extension_headers passes an unknown next_header through untouched") {
+ std::vector<unsigned char> payload = {0x01, 0x02};
+ auto result = walk_ipv6_extension_headers(200, payload); // not a known extension type
+ CHECK(result.final_next_header == 200);
+ REQUIRE(result.payload.size() == 2);
+ CHECK(result.payload[0] == 0x01);
+}