From b558153cc118e034dacde0f7392ab9ccffeac1f8 Mon Sep 17 00:00:00 2001 From: Roman Stolyarchuk Date: Thu, 8 Jan 2026 14:42:40 +0300 Subject: [PATCH 1/5] refactor: change size_t to uint16_t for segment and TLV lengths - Updated segment_at, segment_list_bytes_len, tlv_offset, and tlv_bytes_len methods to return uint16_t instead of size_t. - Modified SRv6Tlv structure to use uint16_t for total_len. - Adjusted SRv6TlvIterator constructor and next method to accept and handle uint16_t for length. --- tests/test_srv6_tlv.cxx | 282 ++++++++++++++++++++++++++++++++++++++++ vbvx/srv6_header.hxx | 28 ++-- 2 files changed, 296 insertions(+), 14 deletions(-) diff --git a/tests/test_srv6_tlv.cxx b/tests/test_srv6_tlv.cxx index bec6e5c..7fc2d30 100644 --- a/tests/test_srv6_tlv.cxx +++ b/tests/test_srv6_tlv.cxx @@ -136,3 +136,285 @@ TEST(Srv6TlvTest, TruncatedTlvHandled) { // iterator should detect truncation and return false EXPECT_FALSE(it.next(t)); } + +TEST(Srv6TlvTest, PadNZeroLength) { + // PadN with length 0 should be parsed (length==0, total_len==2). Any + // trailing zero bytes from 8-octet rounding are Pad1 TLVs and should be + // returned as type 0, total_len==1. + const size_t tlv_bytes_needed = 2; // PadN (type + length) + const size_t min_total = 8 + 16 + tlv_bytes_needed; + const size_t hdr_units = (min_total + 7) / 8; + const size_t total_size = hdr_units * 8; + + std::vector data(total_size, 0u); + SRv6Header hdr{}; + hdr.hdr_ext_len = static_cast((total_size / 8) - 1); + hdr.routing_type = 4; + hdr.last_entry = 0; + std::memcpy(data.data(), &hdr, sizeof(hdr)); + for (size_t i = 0; i < 16; ++i) { + data[8 + i] = static_cast(0x10 + i); + } + + size_t tlv_pos = 8 + 16; + data[tlv_pos++] = 0x04; // PadN + data[tlv_pos++] = 0x00; // length = 0 + + HeaderView hv(data.data()); + ASSERT_TRUE(hv); + SRv6TlvIterator it(hv->tlv_first_ptr(), hv->tlv_bytes_len()); + SRv6Tlv t; + + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 4u); + EXPECT_EQ(t.length, 0u); + EXPECT_EQ(t.total_len, 2u); + + // Any remaining bytes should parse as Pad1 TLVs (type 0) + while (it.next(t)) { + EXPECT_EQ(t.type, 0u); + EXPECT_EQ(t.total_len, 1u); + } +} + +TEST(Srv6TlvTest, HmacMinAndInvalidLengths) { + // HMAC TLV with length == 6 is the minimal valid (2 + 4 + 0) and should + // be accepted by SRv6HmacTlvView. A TLV with length == 5 is too short and + // the view should report invalid. + + // Valid minimal HMAC (length=6) + { + const size_t tlv_bytes_needed = 2 + 6; + const size_t min_total = 8 + 16 + tlv_bytes_needed; + const size_t hdr_units = (min_total + 7) / 8; + const size_t total_size = hdr_units * 8; + + std::vector data(total_size, 0u); + SRv6Header hdr{}; + hdr.hdr_ext_len = static_cast((total_size / 8) - 1); + hdr.routing_type = 4; + hdr.last_entry = 0; + std::memcpy(data.data(), &hdr, sizeof(hdr)); + size_t tlv_pos = 8 + 16; + + data[tlv_pos++] = 0x05; // HMAC + data[tlv_pos++] = 6; // length + uint16_t d_res = autoswap(static_cast(0x0000u)); + std::memcpy(&data[tlv_pos], &d_res, 2); + tlv_pos += 2; + uint32_t kid = autoswap(static_cast(0xAABBCCDDu)); + std::memcpy(&data[tlv_pos], &kid, 4); + tlv_pos += 4; + + HeaderView hv(data.data()); + ASSERT_TRUE(hv); + SRv6TlvIterator it(hv->tlv_first_ptr(), hv->tlv_bytes_len()); + SRv6Tlv t; + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 5u); + EXPECT_EQ(t.length, 6u); + + SRv6HmacTlvView hvw{t.value, t.length}; + EXPECT_TRUE(hvw.valid()); + EXPECT_FALSE(hvw.d_bit()); + EXPECT_EQ(hvw.key_id(), 0xAABBCCDDu); + auto h = hvw.hmac(); + EXPECT_EQ(h.size(), 0u); + } + + // Invalid HMAC (length=5) -> view reports invalid + { + const size_t tlv_bytes_needed = 2 + 5; + const size_t min_total = 8 + 16 + tlv_bytes_needed; + const size_t hdr_units = (min_total + 7) / 8; + const size_t total_size = hdr_units * 8; + + std::vector data(total_size, 0u); + SRv6Header hdr{}; + hdr.hdr_ext_len = static_cast((total_size / 8) - 1); + hdr.routing_type = 4; + hdr.last_entry = 0; + std::memcpy(data.data(), &hdr, sizeof(hdr)); + size_t tlv_pos = 8 + 16; + + data[tlv_pos++] = 0x05; // HMAC + data[tlv_pos++] = 5; // length (too short) + // provide 5 bytes of content (2 byte D/res + 3 partial bytes) + data[tlv_pos++] = 0x00; + data[tlv_pos++] = 0x00; + data[tlv_pos++] = 0xAA; + data[tlv_pos++] = 0xBB; + data[tlv_pos++] = 0xCC; + + HeaderView hv(data.data()); + ASSERT_TRUE(hv); + SRv6TlvIterator it(hv->tlv_first_ptr(), hv->tlv_bytes_len()); + SRv6Tlv t; + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 5u); + EXPECT_EQ(t.length, 5u); + + SRv6HmacTlvView hvw{t.value, t.length}; + EXPECT_FALSE(hvw.valid()); + } +} + +TEST(Srv6TlvTest, UnknownTlvAndIncompleteHeader) { + // Unknown TLV type should be returned as-is. If the buffer ends with a + // single trailing byte (type only), iterator should detect the truncated + // header and return false on the next() call. + const size_t tlv_bytes_needed = + 2 + 2 + 1; // unknown TLV (type+len+2) + 1 byte + + const size_t min_total = 8 + 16 + tlv_bytes_needed; + const size_t hdr_units = (min_total + 7) / 8; + const size_t total_size = hdr_units * 8; + + std::vector data(total_size, 0u); + SRv6Header hdr{}; + hdr.hdr_ext_len = static_cast((total_size / 8) - 1); + hdr.routing_type = 4; + hdr.last_entry = 0; + std::memcpy(data.data(), &hdr, sizeof(hdr)); + size_t tlv_pos = 8 + 16; + + data[tlv_pos++] = 0xFF; // unknown type + data[tlv_pos++] = 2; // length + data[tlv_pos++] = 0x11; + data[tlv_pos++] = 0x22; + + // trailing single byte (incomplete TLV header) + data[tlv_pos++] = 0x04; + + HeaderView hv(data.data()); + ASSERT_TRUE(hv); + SRv6TlvIterator it(hv->tlv_first_ptr(), hv->tlv_bytes_len()); + SRv6Tlv t; + + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 0xFFu); + EXPECT_EQ(t.length, 2u); + EXPECT_EQ(t.total_len, 4u); + + // Due to 8-octet rounding, the trailing byte is followed by a zero; this + // parses as PadN with length==0 rather than a truncated header. + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 4u); + EXPECT_EQ(t.length, 0u); +} + +TEST(Srv6TlvTest, MultipleHmacTlvs) { + // Two HMAC TLVs back-to-back: first minimal (len=6), second with 8-byte HMAC + const size_t tlv_bytes_needed = (2 + 6) + (2 + 14); + const size_t min_total = 8 + 16 + tlv_bytes_needed; + const size_t hdr_units = (min_total + 7) / 8; + const size_t total_size = hdr_units * 8; + + std::vector data(total_size, 0u); + SRv6Header hdr{}; + hdr.hdr_ext_len = static_cast((total_size / 8) - 1); + hdr.routing_type = 4; + hdr.last_entry = 0; + std::memcpy(data.data(), &hdr, sizeof(hdr)); + size_t tlv_pos = 8 + 16; + + // First HMAC TLV (minimal) + data[tlv_pos++] = 0x05; + data[tlv_pos++] = 6; + uint16_t d_res0 = autoswap(static_cast(0x0000u)); + std::memcpy(&data[tlv_pos], &d_res0, 2); + tlv_pos += 2; + uint32_t kid0 = autoswap(static_cast(0xA1A2A3A4u)); + std::memcpy(&data[tlv_pos], &kid0, 4); + tlv_pos += 4; + + // Second HMAC TLV (with D bit and 8-byte HMAC) + data[tlv_pos++] = 0x05; + data[tlv_pos++] = 14; + uint16_t d_res1 = autoswap(static_cast(0x8000u)); + std::memcpy(&data[tlv_pos], &d_res1, 2); + tlv_pos += 2; + uint32_t kid1 = autoswap(static_cast(0x01020304u)); + std::memcpy(&data[tlv_pos], &kid1, 4); + tlv_pos += 4; + for (uint8_t i = 0; i < 8; ++i) { + data[tlv_pos++] = static_cast(0xF0 + i); + } + + HeaderView hv(data.data()); + ASSERT_TRUE(hv); + SRv6TlvIterator it(hv->tlv_first_ptr(), hv->tlv_bytes_len()); + SRv6Tlv t; + + // First TLV + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 5u); + EXPECT_EQ(t.length, 6u); + SRv6HmacTlvView h0{t.value, t.length}; + EXPECT_TRUE(h0.valid()); + EXPECT_FALSE(h0.d_bit()); + EXPECT_EQ(h0.key_id(), 0xA1A2A3A4u); + EXPECT_EQ(h0.hmac().size(), 0u); + + // Second TLV + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 5u); + EXPECT_EQ(t.length, 14u); + SRv6HmacTlvView h1{t.value, t.length}; + EXPECT_TRUE(h1.valid()); + EXPECT_TRUE(h1.d_bit()); + EXPECT_EQ(h1.key_id(), 0x01020304u); + auto h = h1.hmac(); + ASSERT_EQ(h.size(), 8u); + for (size_t i = 0; i < h.size(); ++i) { + EXPECT_EQ(h[i], static_cast(0xF0 + i)); + } + + // Any padding bytes should be Pad1 or PadN as handled elsewhere + while (it.next(t)) { + EXPECT_TRUE(t.type == 0u || t.type == 4u); + } +} + +TEST(Srv6TlvTest, MalformedChainedTlv) { + // First TLV valid, second TLV declares a length much larger than remaining + // bytes — iterator should parse the first TLV and then return false when + // encountering the malformed second TLV. + const size_t tlv_bytes_needed = + 3 + 2 + 1; // first (3 bytes), second header (2) + 1 byte content + const size_t min_total = 8 + 16 + tlv_bytes_needed; + const size_t hdr_units = (min_total + 7) / 8; + const size_t total_size = hdr_units * 8; + + std::vector data(total_size, 0u); + SRv6Header hdr{}; + hdr.hdr_ext_len = static_cast((total_size / 8) - 1); + hdr.routing_type = 4; + hdr.last_entry = 0; + std::memcpy(data.data(), &hdr, sizeof(hdr)); + size_t tlv_pos = 8 + 16; + + // First TLV: unknown type 0x11, length=1, value=0xAA + data[tlv_pos++] = 0x11; + data[tlv_pos++] = 1; + data[tlv_pos++] = 0xAA; + + // Second TLV: unknown type 0x12, length=100 -> truncated by buffer + data[tlv_pos++] = 0x12; + data[tlv_pos++] = 100; + data[tlv_pos++] = 0x55; // one byte of content only + + HeaderView hv(data.data()); + ASSERT_TRUE(hv); + SRv6TlvIterator it(hv->tlv_first_ptr(), hv->tlv_bytes_len()); + SRv6Tlv t; + + // First TLV should be returned + ASSERT_TRUE(it.next(t)); + EXPECT_EQ(t.type, 0x11u); + EXPECT_EQ(t.length, 1u); + EXPECT_EQ(t.total_len, 3u); + + // Second TLV is malformed/truncated; iterator should return false + EXPECT_FALSE(it.next(t)); +} diff --git a/vbvx/srv6_header.hxx b/vbvx/srv6_header.hxx index 656637e..135be0f 100644 --- a/vbvx/srv6_header.hxx +++ b/vbvx/srv6_header.hxx @@ -56,23 +56,23 @@ struct [[gnu::packed]] SRv6Header { constexpr auto segment_at(uint8_t idx) const noexcept -> std::span { return std::span{ - segment_list_ptr() + (static_cast(idx) * 16u), 16u}; + segment_list_ptr() + (static_cast(idx) * 16u), 16u}; } - constexpr auto segment_list_bytes_len() const noexcept -> size_t { - return static_cast(segments_count()) * 16u; + constexpr auto segment_list_bytes_len() const noexcept -> uint16_t { + return static_cast(segments_count()) * 16u; } - constexpr auto tlv_offset() const noexcept -> size_t { + constexpr auto tlv_offset() const noexcept -> uint16_t { return 8u + segment_list_bytes_len(); } - constexpr auto tlv_bytes_len() const noexcept -> size_t { + constexpr auto tlv_bytes_len() const noexcept -> uint16_t { auto total = header_length_bytes(); if (total <= tlv_offset()) { return 0u; } - return static_cast(total - tlv_offset()); + return total - tlv_offset(); } constexpr auto tlv_first_ptr() const noexcept -> const uint8_t* { @@ -85,13 +85,13 @@ struct SRv6Tlv { uint8_t type; uint8_t length; const uint8_t* value; - size_t total_len; + uint16_t total_len; }; /** @brief Iterator over TLVs in an SRH's TLV area. Does not allocate. */ class SRv6TlvIterator { public: - constexpr SRv6TlvIterator(const uint8_t* ptr, size_t len) noexcept + constexpr SRv6TlvIterator(const uint8_t* ptr, uint16_t len) noexcept : ptr_{ptr}, len_{len}, pos_{0} {} constexpr bool next(SRv6Tlv& out) noexcept { @@ -112,19 +112,19 @@ public: } const uint8_t len = ptr_[pos_ + 1u]; - if (static_cast(pos_) + 2u + static_cast(len) > len_) { + if (static_cast(pos_ + 2u + len) > len_) { return false; } - out = SRv6Tlv{t, len, ptr_ + pos_ + 2u, 2u + static_cast(len)}; + out = SRv6Tlv{t, len, ptr_ + pos_ + 2u, static_cast(2u + len)}; pos_ += out.total_len; return true; } private: const uint8_t* ptr_; - size_t len_; - size_t pos_; + uint16_t len_; + uint16_t pos_; }; /** @@ -134,7 +134,7 @@ private: */ struct SRv6HmacTlvView { const uint8_t* value; - uint8_t length; // length of the variable data (as in the TLV length field) + uint8_t length; constexpr bool valid() const noexcept { return value && (length >= 6u); } @@ -158,7 +158,7 @@ struct SRv6HmacTlvView { if (!valid()) return {}; const uint8_t* p = value + 6u; - return std::span{p, static_cast(length - 6u)}; + return std::span{p, static_cast(length - 6u)}; } }; From 7956aa94c685633f68115ce6e88ed1202a2ce95a Mon Sep 17 00:00:00 2001 From: Roman Stolyarchuk Date: Thu, 8 Jan 2026 14:43:10 +0300 Subject: [PATCH 2/5] refactor: rename workflow names --- .github/workflows/docs.yml | 2 +- .github/workflows/{ci.yml => test.yml} | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) rename .github/workflows/{ci.yml => test.yml} (77%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 73d65ac..de87d28 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: "Docs CI" +name: Build Documentation on: push: diff --git a/.github/workflows/ci.yml b/.github/workflows/test.yml similarity index 77% rename from .github/workflows/ci.yml rename to .github/workflows/test.yml index 200054d..74159f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,7 @@ name: Build and Test on: - push: - branches: [ main, docs ] - pull_request: - branches: [ main ] + push: {} jobs: build-and-test: @@ -19,7 +16,7 @@ jobs: - name: Install deps run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends ninja-build clang-format + sudo apt-get install -y --no-install-recommends cmake ninja-build - name: Configure run: cmake -G Ninja -B build -S . -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DBUILD_TESTING=ON From 494a71869569b028f5d0005f1648ce3a6db87483 Mon Sep 17 00:00:00 2001 From: Roman Stolyarchuk Date: Thu, 8 Jan 2026 14:46:40 +0300 Subject: [PATCH 3/5] fix: update CI badge links in README for consistency --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e435dac..4b53b74 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # VBVX - VPP Buffer View eXtensions -[![Build and Test](https://github.com/llmxio/vbvx/actions/workflows/ci.yml/badge.svg)](https://github.com/llmxio/vbvx/actions/workflows/ci.yml) +[![Build and Test](https://github.com/llmxio/vbvx/actions/workflows/test.yml/badge.svg)](https://github.com/llmxio/vbvx/actions/workflows/test.yml) +[![Build Documentation](https://github.com/llmxio/vbvx/actions/workflows/docs.yml/badge.svg)](https://github.com/llmxio/vbvx/actions/workflows/docs.yml) VBVX (VPP Buffer View eXtensions) is a small, header-only C++23 library for **zero-copy** parsing of packet buffers. It provides views over common on-wire headers (Ethernet, VLAN, ARP, IPv4/v6, TCP/UDP, ICMP, SRv6) without copying. From c6d6a54fe59a2479713ec6bfc0ad13405db76e87 Mon Sep 17 00:00:00 2001 From: Roman Stolyarchuk Date: Thu, 8 Jan 2026 14:48:21 +0300 Subject: [PATCH 4/5] refactor: fix method name in BufferView description in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b53b74..7ccc594 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ VBVX (VPP Buffer View eXtensions) is a small, header-only C++23 library for **ze - **Purpose:** Safe, zero-copy access to common wire-protocol headers for packet parsing/inspection. - **Core abstractions:** - - `BufferView` - parses offsets and exposes header views and helper accessors (e.g., `ethertype()`, `l3_offset()`, `l4_offset()`). + - `BufferView` - parses offsets and exposes header views and helper accessors (e.g., `ether_type()`, `l3_offset()`, `l4_offset()`). - `HeaderView` - lightweight wrapper around `const H*` with a `.copy()` helper when a local value is needed. - `FlagsView` / `ConstFlagsView` - zero-copy, chainable views for bitmask enums; enable operators by specializing `vbvx::enable_bitmask_operators`. - `vbvx/*` - packed POD header structs with compile-time checks for layout and alignment. From 236c8c03c48e883392b9872530ee086802d19fc4 Mon Sep 17 00:00:00 2001 From: Roman Stolyarchuk Date: Thu, 8 Jan 2026 15:15:54 +0300 Subject: [PATCH 5/5] refactor: simplify documentation build process and update README badge - remove unused Python3 dependency and post-processing command - rename workflow from "Build Documentation" to "Build Docs" - update README badge link for documentation build to match new name --- .github/workflows/docs.yml | 5 ++--- CMakeLists.txt | 11 ----------- README.md | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index de87d28..c565231 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: Build Documentation +name: Build Docs on: push: @@ -13,7 +13,6 @@ permissions: jobs: build-docs: - name: Build documentation runs-on: ubuntu-latest environment: @@ -31,7 +30,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y graphviz cmake ninja-build python3 python3-distlib + sudo apt-get install -y graphviz cmake ninja-build - name: Cache Doxygen id: cache-doxygen diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a334c9..ea34b7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,23 +90,12 @@ if(BUILD_DOCS) set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) - find_package(Python3 COMPONENTS Interpreter QUIET) - add_custom_target(docs COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating API documentation with Doxygen" VERBATIM) - if(Python3_Interpreter_FOUND) - add_custom_command(TARGET docs POST_BUILD - COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/docs/fix_doxygen_html.py ${CMAKE_CURRENT_BINARY_DIR}/docs/html - COMMENT "Post-processing Doxygen HTML: converting to tags" - VERBATIM) - else() - message(WARNING "Python3 not found: skipping Doxygen HTML post-processing (fixing tags).") - endif() - # Optional: install generated HTML install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs/html DESTINATION ${CMAKE_INSTALL_DOCDIR} diff --git a/README.md b/README.md index 7ccc594..57ab8f5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # VBVX - VPP Buffer View eXtensions [![Build and Test](https://github.com/llmxio/vbvx/actions/workflows/test.yml/badge.svg)](https://github.com/llmxio/vbvx/actions/workflows/test.yml) -[![Build Documentation](https://github.com/llmxio/vbvx/actions/workflows/docs.yml/badge.svg)](https://github.com/llmxio/vbvx/actions/workflows/docs.yml) +[![Build Docs](https://github.com/llmxio/vbvx/actions/workflows/docs.yml/badge.svg)](https://github.com/llmxio/vbvx/actions/workflows/docs.yml) VBVX (VPP Buffer View eXtensions) is a small, header-only C++23 library for **zero-copy** parsing of packet buffers. It provides views over common on-wire headers (Ethernet, VLAN, ARP, IPv4/v6, TCP/UDP, ICMP, SRv6) without copying.