Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Docs CI"
name: Build Docs

on:
push:
Expand All @@ -13,7 +13,6 @@ permissions:

jobs:
build-docs:
name: Build documentation
runs-on: ubuntu-latest

environment:
Expand All @@ -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
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
name: Build and Test

on:
push:
branches: [ main, docs ]
pull_request:
branches: [ main ]
push: {}

jobs:
build-and-test:
Expand All @@ -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
Expand Down
11 changes: 0 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span class=\"tt\"> to <code> tags"
VERBATIM)
else()
message(WARNING "Python3 not found: skipping Doxygen HTML post-processing (fixing <tt> tags).")
endif()

# Optional: install generated HTML
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs/html
DESTINATION ${CMAKE_INSTALL_DOCDIR}
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# 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 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.

## About

- **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<H>` - 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<YourEnum>`.
- `vbvx/*` - packed POD header structs with compile-time checks for layout and alignment.
Expand Down
282 changes: 282 additions & 0 deletions tests/test_srv6_tlv.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> data(total_size, 0u);
SRv6Header hdr{};
hdr.hdr_ext_len = static_cast<uint8_t>((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<uint8_t>(0x10 + i);
}

size_t tlv_pos = 8 + 16;
data[tlv_pos++] = 0x04; // PadN
data[tlv_pos++] = 0x00; // length = 0

HeaderView<SRv6Header> 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<uint8_t> data(total_size, 0u);
SRv6Header hdr{};
hdr.hdr_ext_len = static_cast<uint8_t>((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<uint16_t>(0x0000u));
std::memcpy(&data[tlv_pos], &d_res, 2);
tlv_pos += 2;
uint32_t kid = autoswap(static_cast<uint32_t>(0xAABBCCDDu));
std::memcpy(&data[tlv_pos], &kid, 4);
tlv_pos += 4;

HeaderView<SRv6Header> 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<uint8_t> data(total_size, 0u);
SRv6Header hdr{};
hdr.hdr_ext_len = static_cast<uint8_t>((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<SRv6Header> 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<uint8_t> data(total_size, 0u);
SRv6Header hdr{};
hdr.hdr_ext_len = static_cast<uint8_t>((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<SRv6Header> 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<uint8_t> data(total_size, 0u);
SRv6Header hdr{};
hdr.hdr_ext_len = static_cast<uint8_t>((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<uint16_t>(0x0000u));
std::memcpy(&data[tlv_pos], &d_res0, 2);
tlv_pos += 2;
uint32_t kid0 = autoswap(static_cast<uint32_t>(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<uint16_t>(0x8000u));
std::memcpy(&data[tlv_pos], &d_res1, 2);
tlv_pos += 2;
uint32_t kid1 = autoswap(static_cast<uint32_t>(0x01020304u));
std::memcpy(&data[tlv_pos], &kid1, 4);
tlv_pos += 4;
for (uint8_t i = 0; i < 8; ++i) {
data[tlv_pos++] = static_cast<uint8_t>(0xF0 + i);
}

HeaderView<SRv6Header> 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<uint8_t>(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<uint8_t> data(total_size, 0u);
SRv6Header hdr{};
hdr.hdr_ext_len = static_cast<uint8_t>((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<SRv6Header> 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));
}
Loading