Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VRF implementation for dataplane #242

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

VRF implementation for dataplane #242

wants to merge 7 commits into from

Conversation

stal76
Copy link
Collaborator

@stal76 stal76 commented Sep 8, 2024

Previously, it was possible to add routing rules for different VRFs, but it was not possible to set VRFs for packets and only 'default' was used for them.
In the current change, the ability to set VRF for the entire logical port has been added. Later it will be possible to set VRF by acl rules.
To solve the LPM problem, an algorithm like to 'a path-compressed trie' is used.

@@ -103,6 +103,7 @@ extern LogPriority logPriority;
#define YANET_RIB_PRIORITY_DEFAULT ((uint32_t)10000)
#define YANET_RIB_PRIORITY_ROUTE_TUNNEL_FALLBACK ((uint32_t)11000)
#define YANET_RIB_PRIORITY_ROUTE_REPEAT ((uint32_t)12000)
#define YANET_RIB_VRF_DEFAULT "default"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constexpr auto YANET_RIB_VRF_DEFAULT = "default";

Comment on lines 105 to +121
#define YANET_RIB_PRIORITY_ROUTE_REPEAT ((uint32_t)12000)
#define YANET_RIB_VRF_DEFAULT "default"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should replace that in 97th line in librib/libyabird.cpp too:

		std::get<1>(request) = {peer_address,
		                        {"default", ///< @todo: vrf
		                         YANET_RIB_PRIORITY_DEFAULT}};

Comment on lines +172 to +181
bool is_ignored_table(const std::string& table_name) const
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const method returning value should be marked [[nodiscard]]

Comment on lines 172 to 192
bool is_ignored_table(const std::string& table_name) const
{
for (const auto& [name, module] : routes)
{
(void)name;
if (exist(module.ignore_tables, table_name))
{
return true;
}
}
return false;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we checking that exist is true for at least one element of the sequence, so I suggest to implement it like this:

[[nodiscard]] bool is_ignored_table(const std::string& table_name) const
{
	return std::any_of(routes.begin(), routes.end(), [&table_name](const auto& pair) {
		return exist(pair.second.ignore_tables, table_name);
	});
}

cli/show.h Outdated
Comment on lines 91 to 93
std::get<3>(logicalPort) ? "true" : "false");
std::get<3>(logicalPort),
std::get<4>(logicalPort) ? "true" : "false");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this is not your code initially, but I would do

const auto& [physicalPortName, vlanId, vrf, macAddress, promiscuousMode] = logicalPort;

table.insert(logicalPortName,
             physicalPortName,
             vlanId,
             vrf,
             macAddress,
             promiscuousMode ? "true" : "false");

here to avoid confusion

{
available = 0;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my above proposal is correct, add

void ReserveFirstN(uint8_t n)
{
	for (uint8_t i = 0; i < n; ++i)
	{
		available.reset(i);
	}
}

Comment on lines 134 to 136
uint8_t result = common::bits::get_last_enabled_bit_64(available);
common::bits::disable_bit_64(available, result);
return result;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint8_t first_available = available._Find_first();
available.reset(first_available);
return first_available;


void Free(uint8_t bit)
{
common::bits::enable_bit_64(available, bit);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

available.set(bit);

Comment on lines 1 to 409
}

TEST(Bits, build_mask)
{
for (uint8_t length = 0; length <= 32; length++)
{
uint32_t value = 0;
for (uint8_t bit = 0; bit < length; bit++)
{
common::bits::enable_bit_32(value, 31 - bit);
}
ASSERT_EQ(common::bits::build_mask_32(length), value);
}

for (uint8_t length = 0; length <= 64; length++)
{
uint64_t value = 0;
for (uint8_t bit = 0; bit < length; bit++)
{
common::bits::enable_bit_64(value, 63 - bit);
}
ASSERT_EQ(common::bits::build_mask_64(length), value);
}
}

uint8_t TestFirstEnabledBit32(uint64_t value)
{
for (uint8_t index = 0; index < 32; index++)
{
if (common::bits::get_bit_32(value, 31 - index) == 1)
{
return index;
}
}
return 32;
}

uint8_t TestFirstEnabledBit64(uint64_t value)
{
for (uint8_t index = 0; index < 64; index++)
{
if (common::bits::get_bit_64(value, 63 - index) == 1)
{
return index;
}
}
return 64;
}

TEST(Bits, get_first_enabled_bit)
{
for (uint8_t length = 0; length <= 32; length++)
{
uint32_t mask = common::bits::build_mask_32(length);
ASSERT_EQ(common::bits::get_first_enabled_bit_32(~mask), length);
}

for (uint8_t length = 0; length <= 64; length++)
{
uint64_t mask = common::bits::build_mask_64(length);
ASSERT_EQ(common::bits::get_first_enabled_bit_64(~mask), length);
}

Test32(common::bits::get_first_enabled_bit_32, TestFirstEnabledBit32);
Test64(common::bits::get_first_enabled_bit_64, TestFirstEnabledBit64);
}

template<typename T>
uint8_t TestGetLastEnabledBit(T value)
{
for (uint8_t index = 0; index < 64; index++)
{
if (value % 2 == 1)
{
return index;
}
value /= 2;
}
return sizeof(T) * 8;
}

TEST(Bits, get_last_enabled_bit)
{
for (uint8_t length = 0; length <= 32; length++)
{
uint32_t mask = common::bits::build_mask_32(length);
ASSERT_EQ(common::bits::get_last_enabled_bit_32(mask), 32 - length);
}

for (uint8_t length = 0; length <= 64; length++)
{
uint64_t mask = common::bits::build_mask_64(length);
ASSERT_EQ(common::bits::get_last_enabled_bit_64(mask), 64 - length);
}

Test32(common::bits::get_last_enabled_bit_32, TestGetLastEnabledBit<uint32_t>);
Test64(common::bits::get_last_enabled_bit_64, TestGetLastEnabledBit<uint64_t>);
}

} // namespace
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed

Comment on lines 1 to 8
#include <gtest/gtest.h>
#include <memory>

#include "../blocks_allocator.h"

namespace
{

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are good. Though I would add a test that covers a case where number_first_reserved gets equal to blocks_total_, smth like this should work:

TEST(BlocksAllocator, SequentialAllocationFull)
{
	size_t blocks_number = 1024;
	uint32_t reserved = blocks_number;

	size_t size = common::allocator::BlocksAllocator<64>::GetBufferSize(blocks_number);
	std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size);
	common::allocator::BlocksAllocator<64> allocator;
	allocator.Init(buffer.get(), size, reserved);

	// no more blocks shoudl be available
	ASSERT_EQ(allocator.Allocate(), common::allocator::BlocksAllocator<64>::null_block);
	ASSERT_EQ(allocator.Stat(), std::make_pair(blocks_number, blocks_number));

	for (size_t i = 0; i < reserved; i++)
	{
		ASSERT_EQ(allocator[i], buffer.get() + 64 * i);
	}
}

dataplane/vrf.h Outdated
Comment on lines 15 to 16
constexpr inline uint32_t lpmValueIdInvalid = dataplane::lpmValueIdInvalid;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just using dataplane::lpmValueIdInvalid;

dataplane/vrf.h Outdated
Comment on lines 33 to 34
eResult Insert(stats_t& stats, tVrfId vrfId, const uint32_t& ipAddress, const uint8_t& mask, const uint32_t& valueId);
eResult Remove(stats_t& stats, tVrfId vrfId, const uint32_t& ipAddress, const uint8_t& mask);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using const references with simple fixed-size integer types is redunant and obscure, I would suggest to remove that:

  eResult Insert(Stats& stats, tVrfId vrf_id, uint32_t ip_address, uint8_t mask, uint32_t value_id);
  eResult Remove(Stats& stats, tVrfId vrf_id, uint32_t ip_address, uint8_t mask);

dataplane/vrf.h Outdated
Comment on lines 37 to 38
void Swap(VrfLpm4Linear& other);
void Lookup(const uint32_t* ipAddresses, const tVrfId* vrfIds, uint32_t* valueIds, const unsigned int& count) const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

annotate Swap with noexcept

dataplane/vrf.h Outdated
eResult Clear();
eResult CopyFrom(const VrfLpm4Linear& other, stats_t& stats);
void Swap(VrfLpm4Linear& other);
void Lookup(const uint32_t* ipAddresses, const tVrfId* vrfIds, uint32_t* valueIds, const unsigned int& count) const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const unsigned int& count -> int count

dataplane/vrf.h Outdated
Comment on lines 43 to 44
{
VrfLpm4Linear::BlocksAllocator::Index next_block;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to add VrfLpm4Linear:: here, we're already in that namespace, just BlocksAllocator::Index next_block;

Comment on lines 100 to 109
while (block != nullptr)
{
if (block->ipAddress == ipAddress && block->mask == mask)
{
block->used = 0;
UpdateStats(allocator_, stats);
return eResult::success;
}
block = Block(block->next_block);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here unordered_map will work too, by just finding the block pointer in O(1)

Comment on lines 37 to 38
{
if (vrfId >= YANET_RIB_VRF_MAX_NUMBER)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use extended_chunks_size_min instead of YANET_RIB_VRF_MAX_NUMBER, we done
constexpr static uint64_t extended_chunks_size_min = YANET_RIB_VRF_MAX_NUMBER; for a reason

And in 13 other places in this commit where YANET_RIB_VRF_MAX_NUMBER gets used

Comment on lines 122 to 124
for (unsigned index = 0; index < YANET_RIB_VRF_MAX_NUMBER; index++)
{
OneBlock* block = Block(index);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Segmentation fault.
What if we create a VrfLpm4Linear with an allocator with it's void Init(void* pointer, size_t size, Index number_first_reserved) being called with size less than YANET_RIB_VRF_MAX_NUMBER * BlockSize?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this class is designed for VRF, we should add some guard against initializing that class with buffers less than vrf max number times BlockSize with a "throw" in a constuctor, for example

Comment on lines 208 to 211
VrfLpm4Linear::OneBlock* VrfLpm4Linear::Block(BlocksAllocator::Index index) const
{
return reinterpret_cast<VrfLpm4Linear::OneBlock*>((index == BlocksAllocator::null_block ? nullptr : allocator_[index]));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is cleaner:

VrfLpm4Linear::OneBlock* VrfLpm4Linear::Block(BlocksAllocator::Index index) const
{
	if (index == BlocksAllocator::null_block)
	{
		return nullptr;
	}

	return reinterpret_cast<OneBlock*>(allocator_[index]);
}

Comment on lines 217 to 225
VrfLpm6Linear::VrfLpm6Linear(BlocksAllocator& allocator) :
allocator_(allocator)
{
Clear();
}

eResult VrfLpm6Linear::Insert(stats_t& stats, tVrfId vrfId, const std::array<uint8_t, 16>& ipAddress, const uint8_t& mask, const uint32_t& valueId)
{
if (vrfId >= YANET_RIB_VRF_MAX_NUMBER)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same review comment as for VrfLpm4Linear applies for VrfLpm6Linear too

Comment on lines 12 to 16
'sdp.cpp',
'random_prefixes.cpp',
'vrf_lpm_linear.cpp',
'../vrf.cpp',
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done like in controlplane/unittest/meson.build, with sources defined in a separate variable:

dataplane_sources = files('../vrf.cpp',
                         )

sources = files('unittest.cpp',
                'ip_address.cpp',
                'hashtable.cpp',
                'sdp.cpp',
                'random_prefixes.cpp',
                'vrf_lpm_linear.cpp',
                'vrf_lpm_binary.cpp',
                )

arch = 'corei7'
cpp_args_append = ['-march=' + arch]

unittest = executable('yanet-dataplane-unittest',
                      [dataplane_sources, sources],

Comment on lines 14 to 15
std::string Description() const;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[[nodiscard]]

std::poisson_distribution<> poisson(test_data.depth_lambda);
std::uniform_real_distribution<> uniform(0.0, 1.0);

std::shared_ptr<Node> root = std::make_shared<Node>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto root = std::make_shared<Node>();

Comment on lines 94 to 95
work.push({root, 0});
std::vector<ipv4_prefix_t> prefixes;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use emplace instead of push with a temporary object:

	work.emplace(root, 0);

Comment on lines 104 to 105
prefixes.push_back({addr, static_cast<uint8_t>(node->depth)});
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefixes.emplace_back(addr, static_cast<uint8_t>(node->depth));

Comment on lines 138 to 171
std::vector<ipv6_prefix_t> CreatePrefixesIpv6(const OneTestData& test_data)
{
std::mt19937 generator(test_data.rand_init_value);
std::uniform_int_distribution<> digits_distribution(0, 1);

std::shared_ptr<Node> root = BuildRandomTree(128, test_data, generator);
std::queue<std::pair<std::shared_ptr<Node>, std::array<uint8_t, 16>>> work;
work.push({root, std::array<uint8_t, 16>()});
std::vector<ipv6_prefix_t> prefixes;

while (!work.empty())
{
auto [node, value] = work.front();
work.pop();
if (node->has_value)
{
ipv6_address_t addr = ipv6_address_t::convert(common::ipv6_address_t(value));
prefixes.push_back({addr, static_cast<uint8_t>(node->depth)});
}

if (node->left != nullptr)
{
work.push({node->left, CreatePartAddressIpv6(value, node->depth, node->left->depth, 0, digits_distribution, generator)});
}
if (node->right != nullptr)
{
work.push({node->right, CreatePartAddressIpv6(value, node->depth, node->right->depth, 1, digits_distribution, generator)});
}
}

std::shuffle(prefixes.begin(), prefixes.end(), generator);

return prefixes;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as for v4 version

Comment on lines 13 to 27
TEST(VrfLpm4, SimpleMap)
{
VrfLpmMap<ipv4_prefix_t, uint32_t, uint32_t> vrf_map;
SimpleTestVrfLpm4(vrf_map);
}

TEST(VrfLpm4, SimpleLinear)
{
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size_of_mem);
VrfLpm4Linear::BlocksAllocator allocator;
allocator.Init(buffer.get(), size_of_mem, YANET_RIB_VRF_MAX_NUMBER);
VrfLpm4Linear vrf_linear(allocator);

SimpleTestVrfLpm4(vrf_linear);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could greatly reduce code duplication by utilizing GoogleTests fixtures and TYPED_TEST_SUITE to parametrize tests for both VrfLpmMap and VrfLpmLinear

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here we can use that VrfLpmTestBase to automatically define tests for both v4 and v6

Comment on lines 127 to 128
uint64_t hi = rte_cpu_to_be_64(*(reinterpret_cast<const uint64_t*>(prefix.address.bytes)));
uint64_t low = rte_cpu_to_be_64(*(reinterpret_cast<const uint64_t*>(prefix.address.bytes + 8)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use memcpy when dealing with simple u64 types instead of dereferencing a pointer with reintepret casts, much cleaner this way:

        uint64_t hi = 0, low = 0;

        std::memcpy(&hi, prefix.address.bytes, sizeof(hi));
        std::memcpy(&low, prefix.address.bytes + 8, sizeof(low));
        
        hi = rte_cpu_to_be_64(hi);
        low = rte_cpu_to_be_64(low);

Comment on lines 145 to 146
*(reinterpret_cast<uint64_t*>(result.bytes)) = rte_cpu_to_be_64(hi);
*(reinterpret_cast<uint64_t*>(result.bytes + 8)) = rte_cpu_to_be_64(low);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        hi = rte_cpu_to_be_64(hi);
        low = rte_cpu_to_be_64(low);
        std::memcpy(result.bytes.data(), &hi, sizeof(hi));
        std::memcpy(result.bytes.data() + 8, &low, sizeof(low));

Comment on lines 10 to 38
inline uint32_t IpAddressToInternal(const ipv4_address_t& address)
{
return rte_cpu_to_be_32(address.address);
}

inline std::array<uint8_t, 16> IpAddressToInternal(const ipv6_address_t& address)
{
return common::ipv6_address_t(address.bytes);
}

inline std::string AddressStr(const ipv6_address_t& address)
{
return common::ipv6_address_t(address.bytes).toString();
}

inline std::string AddressStr(const std::array<uint8_t, 16>& address)
{
return common::ipv6_address_t(address).toString();
}

inline std::string AddressStr(const ipv4_address_t& address)
{
return common::ipv4_address_t(address.address).toString();
}

inline std::string AddressStr(const uint32_t& address)
{
return common::ipv4_address_t(address).toString();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are functions that are used only for this .h file, so they should be static instead of inline.

Comment on lines 10 to 38
inline uint32_t IpAddressToInternal(const ipv4_address_t& address)
{
return rte_cpu_to_be_32(address.address);
}

inline std::array<uint8_t, 16> IpAddressToInternal(const ipv6_address_t& address)
{
return common::ipv6_address_t(address.bytes);
}

inline std::string AddressStr(const ipv6_address_t& address)
{
return common::ipv6_address_t(address.bytes).toString();
}

inline std::string AddressStr(const std::array<uint8_t, 16>& address)
{
return common::ipv6_address_t(address).toString();
}

inline std::string AddressStr(const ipv4_address_t& address)
{
return common::ipv4_address_t(address.address).toString();
}

inline std::string AddressStr(const uint32_t& address)
{
return common::ipv4_address_t(address).toString();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking about #242 (comment):

We need to create a struct, smth like AddressTraits, explicitly instantiate it with uint32_t and std::array<uint8_t, 16>, and use it with fixture to parametrize tests.

smth like this:

template<typename AddressType>
struct AddressTraits;

template<>
struct AddressTraits<uint32_t> {
    static uint32_t ToInternal(const ipv4_address_t& address) {
        return rte_cpu_to_be_32(address.address);
    }

    static std::string ToString(const uint32_t& address) {
        return common::ipv4_address_t(address).toString();
    }

    static std::vector<uint32_t> AddressesForTest(const ipv4_prefix_t& prefix) {
        return {rte_cpu_to_be_32(prefix.address.address), LastAddress(prefix)};
    }

    static uint32_t LastAddress(const ipv4_prefix_t& prefix) {
// impl
    }
};

template<>
struct AddressTraits<std::array<uint8_t, 16>> {
    static std::array<uint8_t, 16> ToInternal(const ipv6_address_t& address) {
        return common::ipv6_address_t(address.bytes);
    }

    static std::string ToString(const std::array<uint8_t, 16>& address) {
        return common::ipv6_address_t(address).toString();
    }

    static std::vector<ipv6_address_t> AddressesForTest(const ipv6_prefix_t& prefix) {
        return {prefix.address, LastAddress(prefix)};
    }

    static ipv6_address_t LastAddress(const ipv6_prefix_t& prefix) {
// impl
    }
};

Maybe we should also add to corresponding instantiations these things, consider it

static constexpr OneTestData tests_ipv4[] = {
        {1000, 20.0, 0.8, true, 42, 10}, // tree sizes: 1-4
        {1000, 15.0, 0.8, true, 42, 10}, // tree sizes: 2-7
        {1000, 10.0, 0.8, true, 42, 10}, // tree sizes: 6-15
        {1000, 5.0, 0.8, true, 42, 10}, // tree sizes: 86-271
        {10000, 3.0, 0.1, true, 42, 10}, // tree sizes: 271-992
        {10000, 3.0, 0.2, true, 42, 10} // tree sizes: 574-2053
};

static constexpr size_t number_tests_ipv4 = sizeof(tests_ipv4) / sizeof(OneTestData);

Comment on lines 77 to 117
while ((iter1 != list1.end()) && (iter2 != list2.end()))
{
const auto& [vrf1, address1, mask1, value1] = *iter1;
const auto& [vrf2, address2, mask2, value2] = *iter2;
if ((vrf1 != vrf2) || (address1 != address2) || (mask1 != mask2) || (value1 != value2))
{
YANET_LOG_ERROR("[vrf=%d, %s/%d, value=%d] != [vrf=%d, %s/%d, value=%d]\n",
vrf1,
AddressStr(address1).c_str(),
uint16_t(mask1),
value1,
vrf2,
AddressStr(address2).c_str(),
uint16_t(mask2),
value2);
return false;
}
++iter1;
++iter2;
}
if (iter1 != list1.end())
{
const auto& [vrf1, address1, mask1, value1] = *iter1;
YANET_LOG_ERROR("list2 empty, in list1: vrf=%d, %s/%d, value=%d\n",
vrf1,
AddressStr(address1).c_str(),
uint16_t(mask1),
value1);
return false;
}
if (iter2 != list2.end())
{
const auto& [vrf2, address2, mask2, value2] = *iter2;
YANET_LOG_ERROR("list1 empty, in list2: vrf=%d, %s/%d, value=%d\n",
vrf2,
AddressStr(address2).c_str(),
uint16_t(mask2),
value2);
return false;
}
return true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just list1 == list2? std::vector and std::tuple have == operator

Comment on lines 40 to 118
template<typename PrefixType, typename PrefixTree>
bool FillPrefixTree(const std::vector<PrefixType>& prefixes, PrefixTree& tree, tVrfId vrf_number)
{
uint32_t value = 0;
stats_t stats = {0, 0};
size_t prefix_number = prefixes.size();
for (tVrfId vrf = 0; vrf < vrf_number; vrf++)
{
for (size_t index = 0; index < prefix_number; index += 1 + 5 * vrf)
{
auto address = prefixes[index].address;
if (tree.Insert(stats, vrf, IpAddressToInternal(address), prefixes[index].mask, value) != eResult::success)
{
YANET_LOG_ERROR("Error add prefix, vrf=%d, prefix=%s/%d, value=%d\n",
vrf,
AddressStr(IpAddressToInternal(prefixes[index].address)).c_str(),
(uint16_t)prefixes[index].mask,
value);
return false;
}
value++;
}
}

return true;
}

template<typename PrefixTree1, typename PrefixTree2>
bool CompareTrees(const PrefixTree1& tree1, const PrefixTree2& tree2)
{
auto list1 = tree1.GetFullList();
auto list2 = tree2.GetFullList();
std::sort(list1.begin(), list1.end());
std::sort(list2.begin(), list2.end());
auto iter1 = list1.begin();
auto iter2 = list2.begin();

while ((iter1 != list1.end()) && (iter2 != list2.end()))
{
const auto& [vrf1, address1, mask1, value1] = *iter1;
const auto& [vrf2, address2, mask2, value2] = *iter2;
if ((vrf1 != vrf2) || (address1 != address2) || (mask1 != mask2) || (value1 != value2))
{
YANET_LOG_ERROR("[vrf=%d, %s/%d, value=%d] != [vrf=%d, %s/%d, value=%d]\n",
vrf1,
AddressStr(address1).c_str(),
uint16_t(mask1),
value1,
vrf2,
AddressStr(address2).c_str(),
uint16_t(mask2),
value2);
return false;
}
++iter1;
++iter2;
}
if (iter1 != list1.end())
{
const auto& [vrf1, address1, mask1, value1] = *iter1;
YANET_LOG_ERROR("list2 empty, in list1: vrf=%d, %s/%d, value=%d\n",
vrf1,
AddressStr(address1).c_str(),
uint16_t(mask1),
value1);
return false;
}
if (iter2 != list2.end())
{
const auto& [vrf2, address2, mask2, value2] = *iter2;
YANET_LOG_ERROR("list1 empty, in list2: vrf=%d, %s/%d, value=%d\n",
vrf2,
AddressStr(address2).c_str(),
uint16_t(mask2),
value2);
return false;
}
return true;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And these methods should be within a typed test from which we will inherit later, parametrized by AddressType, like:

template<typename PrefixType, typename AddressType, typename LookupAddressType, typename VrfLpmType>
class VrfLpmTestBase : public ::testing::Test {
protected:
    using Traits = AddressTraits<AddressType>;

    void SetUp() override {
        // common init
    }

    bool FillPrefixTree(const std::vector<PrefixType>& prefixes, VrfLpmType& tree, tVrfId vrf_number) {
        // implementation
        // Use Traits::ToInternalinstead of IpAddressToInternal
    }

    bool CompareTrees(const VrfLpmType& tree1, const VrfLpmType& tree2) {
        auto list1 = tree1.GetFullList();
        auto list2 = tree2.GetFullList();
        std::sort(list1.begin(), list1.end());
        std::sort(list2.begin(), list2.end());
        return list1 == list2;
    }

    bool CheckRequests(tVrfId vrf_number, const std::vector<PrefixType>& prefixes,
                       const VrfLpmType& tree1, const VrfLpmType& tree2) {
        // implementation
        // Use Traits::AddressesForTest instead of AddressesForTest
    }
};

Comment on lines 312 to 361
template<typename VrfLpmType>
void SimpleTestVrfLpm4(VrfLpmType& vrf_lpm)
{
std::vector<std::string> str_prefixes = {"1.0.0.0/24", "2.0.0.0/24", "3.0.0.0/24", "0.0.0.0/0", "1.0.0.0/28", "1.0.0.0/30"};
std::vector<std::string> str_addresses = {"1.0.0.17", "2.0.0.1", "3.0.0.1", "4.0.0.1", "1.0.0.5", "1.0.0.1"};
std::vector<uint32_t> values = {0, 1, 2, 3, 4, 5};

stats_t stats;
uint32_t value = 0;
for (const std::string& str_prefix : str_prefixes)
{
common::ipv4_prefix_t prefix(str_prefix);
ASSERT_EQ(vrf_lpm.Insert(stats, 0, uint32_t(prefix.address()), prefix.mask(), value++), eResult::success);
}

for (size_t index = 0; index < str_addresses.size(); index++)
{
common::ipv4_address_t address(str_addresses[index]);
uint32_t addr = rte_cpu_to_be_32(uint32_t(address));
tVrfId vrfId = 0;
uint32_t value;
vrf_lpm.Lookup(&addr, &vrfId, &value, 1);
ASSERT_EQ(value, values[index]) << "Error check for " << str_addresses[index];
}
}

template<typename VrfLpmType>
void SimpleTestVrfLpm6(VrfLpmType& vrf_lpm)
{
std::vector<std::string> str_prefixes = {"::/0", "7e01::/64", "7e02::/64", "7e03::/64", "7e01::/96", "7e01::f0/124"};
std::vector<std::string> str_addresses = {"7e00::1", "7e01::1:0:0:f1", "7e02::1", "7e03::1", "7e01::1:f1", "7e01::f1"};
std::vector<uint32_t> values = {0, 1, 2, 3, 4, 5};

stats_t stats;
uint32_t value = 0;
for (const std::string& str_prefix : str_prefixes)
{
common::ipv6_prefix_t prefix(str_prefix);
ASSERT_EQ(vrf_lpm.Insert(stats, 0, prefix.address(), prefix.mask(), value++), eResult::success);
}

for (size_t index = 0; index < str_addresses.size(); index++)
{
ipv6_address_t address = ipv6_address_t::convert(common::ipv6_address_t(str_addresses[index]));
tVrfId vrfId = 0;
uint32_t value;
vrf_lpm.Lookup(&address, &vrfId, &value, 1);
ASSERT_EQ(value, values[index]) << "Error check for " << str_addresses[index];
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be added to VrfLpmTestBase as well, so we can use AddressType with things like if constexpr to not duplicate the code.
Those strings can be declared as constexpr static arrays of string_view in the AddressTraits:
constexpr std::array myStrings = { "::/0"sv, "7e01::/64"sv, ...};

Comment on lines 13 to 27
TEST(VrfLpm4, SimpleMap)
{
VrfLpmMap<ipv4_prefix_t, uint32_t, uint32_t> vrf_map;
SimpleTestVrfLpm4(vrf_map);
}

TEST(VrfLpm4, SimpleLinear)
{
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size_of_mem);
VrfLpm4Linear::BlocksAllocator allocator;
allocator.Init(buffer.get(), size_of_mem, YANET_RIB_VRF_MAX_NUMBER);
VrfLpm4Linear vrf_linear(allocator);

SimpleTestVrfLpm4(vrf_linear);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here we can use that VrfLpmTestBase to automatically define tests for both v4 and v6

Comment on lines 738 to 739
if (stats.extended_chunks_size > 2 * ObjectType::extended_chunks_size_min &&
stats.extended_chunks_count < stats.extended_chunks_size / 4)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add should_shrink and use it here

Comment on lines 757 to 758
{
limits.emplace_back(name + ".extended_chunks",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limits.emplace_back(std::string(name) + ".extended_chunks",

eResult create(const uint64_t chunks_size, bool save_old)
{
size_t buffer_size = BlocksAllocator::GetBufferSize(chunks_size);
void* next_pointer = memory_manager->create<void*>(name.data(), socket_id, buffer_size);
if (next_pointer == nullptr)
{
return eResult::errorAllocatingMemory;
}
BlocksAllocator tmp_allocator;
tmp_allocator.Init(next_pointer, buffer_size, YANET_RIB_VRF_MAX_NUMBER);
ObjectType tmp_object(tmp_allocator);

stats_t next_stats;
next_stats.extended_chunks_count = 0;
next_stats.extended_chunks_size = chunks_size;

if (save_old)
{
eResult result = tmp_object.CopyFrom(vrf_lpm_object, next_stats);
if (result != eResult::success)
{
return result;
}
}

stats = next_stats;
vrf_lpm_object.Swap(tmp_object);
if (tmp_allocator.GetPointer() != nullptr)
{
memory_manager->destroy(tmp_allocator.GetPointer());
}

return eResult::success;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current implementation with ObjectType storing reference to an allocator is really obscure. One has no
way of understanding why this code is correct if he don't know that ObjectType actually stores a reference to an allocator. Cause it looks like a regular value semantics, which should lead to an allocator object being in a wrong state.
But here we have:

  • updater_vrf_lpm gets constructed and vrf_lpm_object(allocator) gets called from the member initializer list, storing the reference to that allocator object in an vrf_lpm_object member.
  • user calls init which calls create
  • create creates a tmp_allocator and initializes tmp_object with it. tmp_object now stores a reference to that tmp_allocator
  • vrf_lpm_object.Swap(tmp_object) gets called which swaps allocators, which due to the fact that they are references, swaps the actual allocator object
  • old pointer gets deleted through tmp_object
    Thi is obscure. Also manual memory managing though swapped tmp_object looks bad.

As I suggested eariler, we shouldn't use reference members:
#242 (comment)

Instead, we should:

  1. Delete copy and move constructors from ObjectType. This is really important as apparently the memory that we have there gets managed by an external object, and if that pointer gets copied by a copy constructor, and the original gets deleted, we will have use-after-free
  2. Just create vrf_lpm_object with default constructor in updater_vrf_lpm consturctor, it will have allocator with null pointer, which is fine
  3. ObjectType already has CopyFrom, now we need to implement MoveFrom that accepts memory_manager
    and looks something like this:
void move_from(Obj obj, memory_manager)
{
    if (allocator_.GetPointer() != nullptr) allocator_.GetPointer());
    // move data
}

this way this code gets turned into:

eResult create(const uint64_t chunks_size, bool save_old)
{
	void* next_pointer = memory_manager->create(...)
	BlocksAllocator tmp_allocator;
	tmp_allocator.Init(...);
	ObjectType tmp_object(std::move(tmp_allocator));

	if (save_old)
	{
		tmp_object.CopyFrom(vrf_lpm_object);
	}

	vrf_lpm_object.MoveFrom(tmp_object, memory_manager);

	return eResult::success;
}

And if the vrf_lpm_object was nullptr, memory_manager from MoveFrom will just ignore it.
If it wasn't, it will correctly delete it and fill with the new data.

This way we will ensure that ObjectType has correct ownership of an allocator due to it being moved into a constructor
4. Delete BlocksAllocator allocator; as a field, why do we even need it here?
report(nlohmann::json& report) should get a pointer from an actual object anyway, like vrf_lpm_object.GetAllocator().GetPointer()

Comment on lines 19 to 20
*(uint64_t*)result.data() = hi;
*(uint64_t*)(result.data() + 8) = low;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    auto hi_bytes = reinterpret_cast<const uint8_t*>(&hi);
    auto low_bytes = reinterpret_cast<const uint8_t*>(&low);

    std::copy(hi_bytes, hi_bytes + sizeof(hi), result.begin());
    std::copy(low_bytes, low_bytes + sizeof(low), result.begin() + sizeof(hi));

Comment on lines 233 to 246
VrfLpm4BinaryTree::VrfLpm4BinaryTree(BlocksAllocator& allocator) :
allocator_(allocator)
{
Init();
}

eResult VrfLpm4BinaryTree::Insert(stats_t& stats, tVrfId vrfId, const uint32_t& ipAddress, const uint8_t& mask, const uint32_t& valueId)
{
if (mask > 32 || valueId & 0xFF000000)
{
YANET_LOG_DEBUG("invalid prefix or value\n");
return eResult::invalidArguments;
}
if (vrfId >= YANET_RIB_VRF_MAX_NUMBER)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the same issues as with Linear, please.
Like avoidance of reference field member, emplace instead of push with braces initialization: push({...}), should throw in constructor if the size allocated is not enough for YANET_RIB_VRF_MAX_NUMBER, otherwise loop like this

	for (tVrfId vrfId = 0; vrfId < YANET_RIB_VRF_MAX_NUMBER; vrfId++)
	{
		OneBlock* block = Block(vrfId);

will segfault on Block when we'll call allocator_[index]

And also an observation that we can cache the blocks by a pair ipAddress + mask to avoid looking them up each time

- Fixed filling in lpm stats.
- Checking that the route table is ignored has been moved.
- VRF storage has been added in some structures, and vrf is also passed in calls to some functions.
- In controlplane.conf one can set the vrf in logicalPort. In the metadata of the network packet
  the vrfId is filled in according to the value from logicalPort.
- A VrfIdStorage object has been added to the cControlPlane class, which issues an id named vrf.
In controlplane.conf, one can set the vrf for lan and wan in the nat64statefull
and nat46clat sections, these values will be set in the packet metadata.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants