#include #include #include "wireframe/net/ipv4.hpp" // for kProtoTcp #include "wireframe/net/ipv6.hpp" using namespace wireframe::net; namespace { Ipv6Address addr_from_groups(std::array groups) { Ipv6Address addr{}; for (std::size_t i = 0; i < 8; ++i) { addr.bytes[i * 2] = static_cast(groups[i] >> 8); addr.bytes[i * 2 + 1] = static_cast(groups[i] & 0xFF); } return addr; } } // namespace TEST_CASE("parse_ipv6 decodes header fields and leaves the right payload") { std::vector 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 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 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 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 payload = {static_cast(kProtoTcp), 0x00, 0, 0, 0, 0, 0, 0}; // 6 bytes of option padding std::vector 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 payload = { kNextHeaderDestOptions, 0x00, 0, 0, 0, 0, 0, 0, // Hop-by-Hop, len 8 static_cast(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 payload = {static_cast(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 payload(12, 0); payload[0] = static_cast(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 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 payload = {static_cast(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 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); }