Commit e75c686c authored by Marcin Siodelski's avatar Marcin Siodelski
Browse files

[master] Merge branch 'trac2318'

parents 8b36f92a cb8128c9
......@@ -60,6 +60,7 @@ b10_dhcp6_CXXFLAGS = -Wno-unused-parameter
endif
b10_dhcp6_LDADD = $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
......
This diff is collapsed.
......@@ -76,4 +76,6 @@
simple as possible. In fact, currently the code has to call Subnet6->getT1() and
do not implement any fancy inheritance logic.
@todo Add section about setting up options and their definitions with bindctl.
*/
......@@ -40,6 +40,37 @@
"item_default": 4000
},
{ "item_name": "option-data",
"item_type": "list",
"item_optional": false,
"item_default": [],
"list_item_spec":
{
"item_name": "single-option-data",
"item_type": "map",
"item_optional": false,
"item_default": {},
"map_item_spec": [
{
"item_name": "name",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "code",
"item_type": "integer",
"item_optional": false,
"item_default": 0
},
{ "item_name": "data",
"item_type": "string",
"item_optional": false,
"item_default": ""
} ]
}
},
{ "item_name": "subnet6",
"item_type": "list",
"item_optional": false,
......@@ -92,10 +123,40 @@
"item_optional": false,
"item_default": ""
}
}
]
}
}
},
{ "item_name": "option-data",
"item_type": "list",
"item_optional": false,
"item_default": [],
"list_item_spec":
{
"item_name": "single-option-data",
"item_type": "map",
"item_optional": false,
"item_default": {},
"map_item_spec": [
{
"item_name": "name",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{
"item_name": "code",
"item_type": "integer",
"item_optional": false,
"item_default": 0
},
{
"item_name": "data",
"item_type": "string",
"item_optional": false,
"item_default": ""
} ]
}
} ]
}
}
],
"commands": [
{
......
......@@ -129,3 +129,8 @@ This is an informational message announcing the successful processing of a
new configuration. it is output during server startup, and when an updated
configuration is committed by the administrator. Additional information
may be provided.
% DHCP6_CONFIG_OPTION_DUPLICATE multiple options with the code: %1 added to the subnet: %2
This warning message is issued on attempt to configure multiple options with the
same option code for the particular subnet. Adding multiple options is uncommon
for DHCPv6, yet it is not prohibited.
......@@ -63,6 +63,7 @@ dhcp6_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
dhcp6_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
dhcp6_unittests_LDADD = $(GTEST_LDADD)
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcpsrv.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
......
......@@ -49,6 +49,110 @@ public:
delete srv_;
};
/// @brief Create the simple configuration with single option.
///
/// This function allows to set one of the parameters that configure
/// option value. These parameters are: "name", "code" and "data".
///
/// @param param_value string holiding option parameter value to be
/// injected into the configuration string.
/// @param parameter name of the parameter to be configured with
/// param value.
std::string createConfigWithOption(const std::string& param_value,
const std::string& parameter) {
std::ostringstream stream;
stream << "{ \"interface\": [ \"all\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::/80\" ],"
" \"subnet\": \"2001:db8:1::/64\", "
" \"option-data\": [ {";
if (parameter == "name") {
stream <<
" \"name\": \"" << param_value << "\","
" \"code\": 80,"
" \"data\": \"AB CDEF0105\"";
} else if (parameter == "code") {
stream <<
" \"name\": \"option_foo\","
" \"code\": " << param_value << ","
" \"data\": \"AB CDEF0105\"";
} else if (parameter == "data") {
stream <<
" \"name\": \"option_foo\","
" \"code\": 80,"
" \"data\": \"" << param_value << "\"";
}
stream <<
" } ]"
" } ],"
"\"valid-lifetime\": 4000 }";
return (stream.str());
}
/// @brief Test invalid option parameter value.
///
/// This test function constructs the simple configuration
/// string and injects invalid option configuration into it.
/// It expects that parser will fail with provided option code.
///
/// @param param_value string holding invalid option parameter value
/// to be injected into configuration string.
/// @param parameter name of the parameter to be configured with
/// param_value (can be any of "name", "code", "data")
void testInvalidOptionParam(const std::string& param_value,
const std::string& parameter) {
ConstElementPtr x;
std::string config = createConfigWithOption(param_value, parameter);
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(x = configureDhcp6Server(*srv_, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(1, rcode_);
}
/// @brief Test option against given code and data.
///
/// @param option_desc option descriptor that carries the option to
/// be tested.
/// @param expected_code expected code of the option.
/// @param expected_data expected data in the option.
/// @param expected_data_len length of the reference data.
/// @param extra_data if true extra data is allowed in an option
/// after tested data.
void testOption(const Subnet::OptionDescriptor& option_desc,
uint16_t expected_code, const uint8_t* expected_data,
size_t expected_data_len,
bool extra_data = false) {
// Check if option descriptor contains valid option pointer.
ASSERT_TRUE(option_desc.option);
// Verify option type.
EXPECT_EQ(expected_code, option_desc.option->getType());
// We may have many different option types being created. Some of them
// have dedicated classes derived from Option class. In such case if
// we want to verify the option contents against expected_data we have
// to prepare raw buffer with the contents of the option. The easiest
// way is to call pack() which will prepare on-wire data.
util::OutputBuffer buf(option_desc.option->getData().size());
option_desc.option->pack(buf);
if (extra_data) {
// The length of the buffer must be at least equal to size of the
// reference data but it can sometimes be greater than that. This is
// because some options carry suboptions that increase the overall
// length.
ASSERT_GE(buf.getLength() - option_desc.option->getHeaderLen(),
expected_data_len);
} else {
ASSERT_EQ(buf.getLength() - option_desc.option->getHeaderLen(),
expected_data_len);
}
// Verify that the data is correct. However do not verify suboptions.
const uint8_t* data = static_cast<const uint8_t*>(buf.getData());
EXPECT_TRUE(memcmp(expected_data, data, expected_data_len));
}
Dhcpv6Srv* srv_;
int rcode_;
......@@ -73,7 +177,7 @@ TEST_F(Dhcp6ParserTest, version) {
/// The goal of this test is to verify that the code accepts only
/// valid commands and malformed or unsupported parameters are rejected.
TEST_F(Dhcp6ParserTest, bogus_command) {
TEST_F(Dhcp6ParserTest, bogusCommand) {
ConstElementPtr x;
......@@ -89,7 +193,7 @@ TEST_F(Dhcp6ParserTest, bogus_command) {
/// The goal of this test is to verify if wrongly defined subnet will
/// be rejected. Properly defined subnet must include at least one
/// pool definition.
TEST_F(Dhcp6ParserTest, empty_subnet) {
TEST_F(Dhcp6ParserTest, emptySubnet) {
ConstElementPtr status;
......@@ -109,7 +213,7 @@ TEST_F(Dhcp6ParserTest, empty_subnet) {
/// The goal of this test is to verify if defined subnet uses global
/// parameter timer definitions.
TEST_F(Dhcp6ParserTest, subnet_global_defaults) {
TEST_F(Dhcp6ParserTest, subnetGlobalDefaults) {
ConstElementPtr status;
......@@ -144,7 +248,7 @@ TEST_F(Dhcp6ParserTest, subnet_global_defaults) {
// This test checks if it is possible to override global values
// on a per subnet basis.
TEST_F(Dhcp6ParserTest, subnet_local) {
TEST_F(Dhcp6ParserTest, subnetLocal) {
ConstElementPtr status;
......@@ -181,7 +285,7 @@ TEST_F(Dhcp6ParserTest, subnet_local) {
// Test verifies that a subnet with pool values that do not belong to that
// pool are rejected.
TEST_F(Dhcp6ParserTest, pool_out_of_subnet) {
TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
ConstElementPtr status;
......@@ -209,7 +313,7 @@ TEST_F(Dhcp6ParserTest, pool_out_of_subnet) {
// Goal of this test is to verify if pools can be defined
// using prefix/length notation. There is no separate test for min-max
// notation as it was tested in several previous tests.
TEST_F(Dhcp6ParserTest, pool_prefix_len) {
TEST_F(Dhcp6ParserTest, poolPrefixLen) {
ConstElementPtr x;
......@@ -240,4 +344,318 @@ TEST_F(Dhcp6ParserTest, pool_prefix_len) {
EXPECT_EQ(4000, subnet->getValid());
}
// Goal of this test is to verify that global option
// data is configured for the subnet if the subnet
// configuration does not include options configuration.
TEST_F(Dhcp6ParserTest, optionDataDefaults) {
ConstElementPtr x;
string config = "{ \"interface\": [ \"all\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
" \"name\": \"option_foo\","
" \"code\": 100,"
" \"data\": \"AB CDEF0105\""
" },"
" {"
" \"name\": \"option_foo2\","
" \"code\": 101,"
" \"data\": \"01\""
" } ],"
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::/80\" ],"
" \"subnet\": \"2001:db8:1::/64\""
" } ],"
"\"valid-lifetime\": 4000 }";
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(x = configureDhcp6Server(*srv_, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(0, rcode_);
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
ASSERT_TRUE(subnet);
const Subnet::OptionContainer& options = subnet->getOptions();
ASSERT_EQ(2, options.size());
// Get the search index. Index #1 is to search using option code.
const Subnet::OptionContainerTypeIndex& idx = options.get<1>();
// Get the options for specified index. Expecting one option to be
// returned but in theory we may have multiple options with the same
// code so we get the range.
std::pair<Subnet::OptionContainerTypeIndex::const_iterator,
Subnet::OptionContainerTypeIndex::const_iterator> range =
idx.equal_range(100);
// Expect single option with the code equal to 100.
ASSERT_EQ(1, std::distance(range.first, range.second));
const uint8_t foo_expected[] = {
0xAB, 0xCD, 0xEF, 0x01, 0x05
};
// Check if option is valid in terms of code and carried data.
testOption(*range.first, 100, foo_expected, sizeof(foo_expected));
range = idx.equal_range(101);
ASSERT_EQ(1, std::distance(range.first, range.second));
// Do another round of testing with second option.
const uint8_t foo2_expected[] = {
0x01
};
testOption(*range.first, 101, foo2_expected, sizeof(foo2_expected));
// Check that options with other option codes are not returned.
for (uint16_t code = 102; code < 110; ++code) {
range = idx.equal_range(code);
EXPECT_EQ(0, std::distance(range.first, range.second));
}
}
// Goal of this test is to verify options configuration
// for a single subnet. In particular this test checks
// that local options configuration overrides global
// option setting.
TEST_F(Dhcp6ParserTest, optionDataInSingleSubnet) {
ConstElementPtr x;
string config = "{ \"interface\": [ \"all\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"option-data\": [ {"
" \"name\": \"option_foo\","
" \"code\": 100,"
" \"data\": \"AB\""
" } ],"
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::/80\" ],"
" \"subnet\": \"2001:db8:1::/64\", "
" \"option-data\": [ {"
" \"name\": \"option_foo\","
" \"code\": 100,"
" \"data\": \"AB CDEF0105\""
" },"
" {"
" \"name\": \"option_foo2\","
" \"code\": 101,"
" \"data\": \"01\""
" } ]"
" } ],"
"\"valid-lifetime\": 4000 }";
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(x = configureDhcp6Server(*srv_, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(0, rcode_);
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
ASSERT_TRUE(subnet);
const Subnet::OptionContainer& options = subnet->getOptions();
ASSERT_EQ(2, options.size());
// Get the search index. Index #1 is to search using option code.
const Subnet::OptionContainerTypeIndex& idx = options.get<1>();
// Get the options for specified index. Expecting one option to be
// returned but in theory we may have multiple options with the same
// code so we get the range.
std::pair<Subnet::OptionContainerTypeIndex::const_iterator,
Subnet::OptionContainerTypeIndex::const_iterator> range =
idx.equal_range(100);
// Expect single option with the code equal to 100.
ASSERT_EQ(1, std::distance(range.first, range.second));
const uint8_t foo_expected[] = {
0xAB, 0xCD, 0xEF, 0x01, 0x05
};
// Check if option is valid in terms of code and carried data.
testOption(*range.first, 100, foo_expected, sizeof(foo_expected));
range = idx.equal_range(101);
ASSERT_EQ(1, std::distance(range.first, range.second));
// Do another round of testing with second option.
const uint8_t foo2_expected[] = {
0x01
};
testOption(*range.first, 101, foo2_expected, sizeof(foo2_expected));
}
// Goal of this test is to verify options configuration
// for multiple subnets.
TEST_F(Dhcp6ParserTest, optionDataInMultipleSubnets) {
ConstElementPtr x;
string config = "{ \"interface\": [ \"all\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ { "
" \"pool\": [ \"2001:db8:1::/80\" ],"
" \"subnet\": \"2001:db8:1::/64\", "
" \"option-data\": [ {"
" \"name\": \"option_foo\","
" \"code\": 100,"
" \"data\": \"0102030405060708090A\""
" } ]"
" },"
" {"
" \"pool\": [ \"2001:db8:2::/80\" ],"
" \"subnet\": \"2001:db8:2::/64\", "
" \"option-data\": [ {"
" \"name\": \"option_foo2\","
" \"code\": 101,"
" \"data\": \"FFFEFDFCFB\""
" } ]"
" } ],"
"\"valid-lifetime\": 4000 }";
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(x = configureDhcp6Server(*srv_, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(0, rcode_);
Subnet6Ptr subnet1 = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
ASSERT_TRUE(subnet1);
const Subnet::OptionContainer& options1 = subnet1->getOptions();
ASSERT_EQ(1, options1.size());
// Get the search index. Index #1 is to search using option code.
const Subnet::OptionContainerTypeIndex& idx1 = options1.get<1>();
// Get the options for specified index. Expecting one option to be
// returned but in theory we may have multiple options with the same
// code so we get the range.
std::pair<Subnet::OptionContainerTypeIndex::const_iterator,
Subnet::OptionContainerTypeIndex::const_iterator> range1 =
idx1.equal_range(100);
// Expect single option with the code equal to 100.
ASSERT_EQ(1, std::distance(range1.first, range1.second));
const uint8_t foo_expected[] = {
0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0A
};
// Check if option is valid in terms of code and carried data.
testOption(*range1.first, 100, foo_expected, sizeof(foo_expected));
// Test another subnet in the same way.
Subnet6Ptr subnet2 = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:2::4"));
ASSERT_TRUE(subnet2);
const Subnet::OptionContainer& options2 = subnet2->getOptions();
ASSERT_EQ(1, options2.size());
const Subnet::OptionContainerTypeIndex& idx2 = options2.get<1>();
std::pair<Subnet::OptionContainerTypeIndex::const_iterator,
Subnet::OptionContainerTypeIndex::const_iterator> range2 =
idx2.equal_range(101);
ASSERT_EQ(1, std::distance(range2.first, range2.second));
const uint8_t foo2_expected[] = {
0xFF, 0xFE, 0xFD, 0xFC, 0xFB
};
testOption(*range2.first, 101, foo2_expected, sizeof(foo2_expected));
}
// Verify that empty option name is rejected in the configuration.
TEST_F(Dhcp6ParserTest, optionNameEmpty) {
// Empty option names not allowed.
testInvalidOptionParam("", "name");
}
// Verify that empty option name with spaces is rejected
// in the configuration.
TEST_F(Dhcp6ParserTest, optionNameSpaces) {
// Spaces in option names not allowed.
testInvalidOptionParam("option foo", "name");
}
// Verify that negative option code is rejected in the configuration.
TEST_F(Dhcp6ParserTest, optionCodeNegative) {
// Check negative option code -4. This should fail too.
testInvalidOptionParam("-4", "code");
}
// Verify that out of bounds option code is rejected in the configuration.
TEST_F(Dhcp6ParserTest, optionCodeNonUint16) {
// The valid option codes are uint16_t values so passing
// uint16_t maximum value incremented by 1 should result
// in failure.
testInvalidOptionParam("65536", "code");
}
// Verify that out of bounds option code is rejected in the configuration.
TEST_F(Dhcp6ParserTest, optionCodeHighNonUint16) {
// Another check for uint16_t overflow but this time
// let's pass even greater option code value.
testInvalidOptionParam("70000", "code");
}
// Verify that zero option code is rejected in the configuration.
TEST_F(Dhcp6ParserTest, optionCodeZero) {
// Option code 0 is reserved and should not be accepted
// by configuration parser.
testInvalidOptionParam("0", "code");
}
// Verify that option data which contains non hexadecimal characters
// is rejected by the configuration.
TEST_F(Dhcp6ParserTest, optionDataInvalidChar) {
// Option code 0 is reserved and should not be accepted
// by configuration parser.
testInvalidOptionParam("01020R", "data");
}
// Verify that option data containins '0x' prefix is rejected
// by the configuration.
TEST_F(Dhcp6ParserTest, optionDataUnexpectedPrefix) {
// Option code 0 is reserved and should not be accepted
// by configuration parser.
testInvalidOptionParam("0x0102", "data");
}
// Verify that option data consisting od an odd number of
// hexadecimal digits is rejected in the configuration.
TEST_F(Dhcp6ParserTest, optionDataOddLength) {
// Option code 0 is reserved and should not be accepted
// by configuration parser.
testInvalidOptionParam("123", "data");
}
// Verify that either lower or upper case characters are allowed
// to specify the option data.
TEST_F(Dhcp6ParserTest, optionDataLowerCase) {
ConstElementPtr x;
std::string config = createConfigWithOption("0a0b0C0D", "data");
ElementPtr json = Element::fromJSON(config);
EXPECT_NO_THROW(x = configureDhcp6Server(*srv_, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(0, rcode_);
Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
ASSERT_TRUE(subnet);
const Subnet::OptionContainer& options = subnet->getOptions();
ASSERT_EQ(1, options.size());
// Get the search index. Index #1 is to search using option code.
const Subnet::OptionContainerTypeIndex& idx = options.get<1>();
// Get the options for specified index. Expecting one option to be
// returned but in theory we may have multiple options with the same
// code so we get the range.
std::pair<Subnet::OptionContainerTypeIndex::const_iterator,
Subnet::OptionContainerTypeIndex::const_iterator> range =
idx.equal_range(80);
// Expect single option with the code equal to 100.
ASSERT_EQ(1, std::distance(range.first, range.second));
const uint8_t foo_expected[] = {
0x0A, 0x0B, 0x0C, 0x0D
};
// Check if option is valid in terms of code and carried data.
testOption(*range.first, 80, foo_expected, sizeof(foo_expected));
}
};
......@@ -193,6 +193,11 @@ public:
/// Type of the index #1 - option type.
typedef OptionContainer::nth_index<1>::type OptionContainerTypeIndex;
/// Pair of iterators to represent the range of options having the
/// same option type value. The first element in this pair represents