2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-09-02 06:55:16 +00:00

[#1518] implemented support for multiple vendor options in v4

This commit is contained in:
Razvan Becheriu
2023-01-13 15:52:56 +02:00
parent 63b57123ac
commit 216aadb45a
10 changed files with 972 additions and 347 deletions

View File

@@ -2479,16 +2479,15 @@ The standard spaces defined in Kea and their options are:
| 2 | tftp-servers | a list of IPv4 addresses of TFTP servers to be used by the cable modem |
+-------------+--------------+------------------------------------------------------------------------+
In Kea each vendor is represented by its own vendor space. Since there
are hundreds of vendors and sometimes they use different option
definitions for different hardware, it is impossible for Kea to support
them all natively. Fortunately, it's easy to define support for
new vendor options. Let's take an example of the Genexis home gateway. This
device requires sending the vivso 125 option with a sub-option 2 that
contains a string with the TFTP server URL. To support such a device, three
steps are needed: first, we need to define option definitions that will
explain how the option is supposed to be formed. Second, we need to
define option values. Third, we need to tell Kea when to send those
In Kea each vendor is represented by its own vendor space. Since there are
hundreds of vendors and sometimes they use different option definitions for
different hardware, it is impossible for Kea to support them all natively.
Fortunately, it's easy to define support for new vendor options. Let's take an
example of the Genexis home gateway. This device requires sending the vivso 125
option with a sub-option 2 that contains a string with the TFTP server URL. To
support such a device, three steps are needed: first, we need to define option
definitions that will explain how the option is supposed to be formed. Second,
we need to define option values. Third, we need to tell Kea when to send those
specific options, which we can do via client classification.
An example snippet of a configuration could look similar to the
@@ -2497,7 +2496,7 @@ following:
::
{
// First, we need to define that the suboption 2 in vivso option for
// First, we need to define that the sub-option 2 in vivso option for
// vendor-id 25167 has a specific format (it's a plain string in this example).
// After this definition, we can specify values for option tftp.
"option-def": [
@@ -2524,15 +2523,15 @@ following:
"test": "substring(option[60].hex,0,7) == 'HMC1000'",
// Once the device is recognized, we want to send two options:
// the vivso option with vendor-id set to 25167, and a suboption 2.
// the vivso option with vendor-id set to 25167, and a sub-option 2.
"option-data": [
{
"name": "vivso-suboptions",
"data": "25167"
},
// The suboption 2 value is defined as any other option. However,
// we want to send this suboption 2, even when the client didn't
// The sub-option 2 value is defined as any other option. However,
// we want to send this sub-option 2, even when the client didn't
// explicitly request it (often there is no way to do that for
// vendor options). Therefore we use always-send to force Kea
// to always send this option when 25167 vendor space is involved.
@@ -2546,27 +2545,26 @@ following:
} ]
}
By default, Kea sends back
only those options that are requested by a client, unless there are
protocol rules that tell the DHCP server to always send an option. This
approach works nicely in most cases and avoids problems with clients
refusing responses with options they do not understand. However,
the situation with vendor options is more complex, as they
are not requested the same way as other options, are
not well-documented in official RFCs, or vary by vendor.
By default, Kea sends back only those options that are requested by a client,
unless there are protocol rules that tell the DHCP server to always send an
option. This approach works nicely in most cases and avoids problems with
clients refusing responses with options they do not understand. However, the
situation with vendor options is more complex, as they are not requested the
same way as other options, are not well-documented in official RFCs, or vary by
vendor.
Some vendors (such
as DOCSIS, identified by vendor option 4491) have a mechanism to
request specific vendor options and Kea is able to honor those.
Unfortunately, for many other vendors, such as Genexis (25167, discussed
above), Kea does not have such a mechanism, so it cannot send any
sub-options on its own. To solve this issue, we devised the concept of
persistent options. Kea can be told to always send options, even if the
client did not request them. This can be achieved by adding
``"always-send": true`` to the option definition. Note that in this
particular case an option is defined in vendor space 25167. With
``always-send`` enabled, the option is sent every time there is a
need to deal with vendor space 25167.
Some vendors (such as DOCSIS, identified by vendor option 4491) have a mechanism
to request specific vendor options and Kea is able to honor those (sub-option 1).
Unfortunately, for many other vendors, such as Genexis (25167, discussed above),
Kea does not have such a mechanism, so it cannot send any sub-options on its own.
To solve this issue, we devised the concept of persistent options. Kea can be
told to always send options, even if the client did not request them. This can
be achieved by adding ``"always-send": true`` to the option definition. Note
that in this particular case an option is defined in vendor space 25167. With
``always-send`` enabled, the option is sent every time there is a need to deal
with vendor space 25167.
This is also how the Kea server can be configured to send multiple vendor
enterprise numbers and multiple options, specific for each vendor.
Another possibility is to redefine the option; see :ref:`dhcp4-private-opts`.
@@ -2576,10 +2574,16 @@ and ``doc/examples/kea6/vivso.json`` in the Kea sources.
.. note::
Currently only one vendor is supported for the ``vivco-suboptions`` (code 124)
and ``vivso-suboptions`` (code 125) options. Specifying
multiple enterprise numbers within a single option instance or multiple
options with different enterprise numbers is not supported.
Multiple vendor enterprise numbers are supported by ``vivso-suboptions``
(code 125) option. The option can contain multiple options for each vendor.
Kea will honor DOCSIS sub-option 1 (ORO) and will add only requested options
if this sub-option is present in the client request.
Currently only one vendor is supported for the ``vivco-suboptions``
(code 124) option. Specifying multiple enterprise numbers within a single
option instance or multiple options with different enterprise numbers is not
supported.
.. _dhcp4-option-spaces:
@@ -2741,11 +2745,11 @@ Support for Long Options
------------------------
The kea-dhcp4 server partially supports long options (RFC3396).
Since Kea 2.1.6, the server accepts configuring long options and suboptions
(longer than 255 bytes). The options and suboptions are stored internally
Since Kea 2.1.6, the server accepts configuring long options and sub-options
(longer than 255 bytes). The options and sub-options are stored internally
in their unwrapped form and they can be processed as usual using the parser
language. On send, the server splits long options and suboptions into multiple
options and suboptions, using the respective option code.
language. On send, the server splits long options and sub-options into multiple
options and sub-options, using the respective option code.
::
@@ -2800,8 +2804,8 @@ into two options, each with the code 240.
The server is also able to receive packets with split options (options using
the same option code) and to fuse the data chunks into one option. This is
also supported for suboptions if each suboption data chunk also contains the
suboption code and suboption length.
also supported for sub-options if each sub-option data chunk also contains the
sub-option code and sub-option length.
.. _dhcp4-stateless-configuration:
@@ -4084,7 +4088,7 @@ retained on the lease. The lease's user-context looks something like this:
{ "ISC": { "relay-agent-info": { "sub-options": "0x0104AABBCCDD" } } }
Or with remote and relay suboptions:
Or with remote and relay sub-options:
::
@@ -7292,12 +7296,12 @@ Ignore RAI Link Selection
-------------------------
With ``"ignore-rai-link-selection": true``, Relay Agent Information Link
Selection suboption data will not be used for subnet selection. This will use
Selection sub-option data will not be used for subnet selection. This will use
normal subnet selection logic instead of attempting to use the subnet specified
by the suboption. This option is not RFC compliant and is set to ``false`` by
by the sub-option. This option is not RFC compliant and is set to ``false`` by
default. Setting this option to ``true`` can help with subnet selection in
certain scenarios, for example, when your DHCP relays do not allow you to
specify which suboptions are included in the Relay Agent Information option,
specify which sub-options are included in the Relay Agent Information option,
and include incorrect Link Selection information.
.. code-block:: json

View File

@@ -1736,19 +1736,15 @@ Dhcpv4Srv::buildCfgOptionList(Dhcpv4Exchange& ex) {
// Retrieve subnet.
Subnet4Ptr subnet = ex.getContext()->subnet_;
if (!subnet) {
// All methods using the CfgOptionList object return soon when
// there is no subnet so do the same
return;
}
// Firstly, host specific options.
const ConstHostPtr& host = ex.getContext()->currentHost();
if (host && !host->getCfgOption4()->empty()) {
co_list.push_back(host->getCfgOption4());
}
// Secondly, pool specific options.
// Secondly, pool specific options. Pools are defined within a subnet, so
// if there is no subnet, there is nothing to do.
if (subnet) {
Pkt4Ptr resp = ex.getResponse();
IOAddress addr = IOAddress::IPV4_ZERO_ADDRESS();
if (resp) {
@@ -1772,6 +1768,7 @@ Dhcpv4Srv::buildCfgOptionList(Dhcpv4Exchange& ex) {
if (network && !network->getCfgOption()->empty()) {
co_list.push_back(network->getCfgOption());
}
}
// Each class in the incoming packet
const ClientClasses& classes = ex.getQuery()->getClasses();
@@ -1807,17 +1804,6 @@ Dhcpv4Srv::buildCfgOptionList(Dhcpv4Exchange& ex) {
void
Dhcpv4Srv::appendRequestedOptions(Dhcpv4Exchange& ex) {
// Get the subnet relevant for the client. We will need it
// to get the options associated with it.
Subnet4Ptr subnet = ex.getContext()->subnet_;
// If we can't find the subnet for the client there is no way
// to get the options to be sent to a client. We don't log an
// error because it will be logged by the assignLease method
// anyway.
if (!subnet) {
return;
}
// Unlikely short cut
const CfgOptionList& co_list = ex.getCfgOptionList();
if (co_list.empty()) {
@@ -1826,20 +1812,23 @@ Dhcpv4Srv::appendRequestedOptions(Dhcpv4Exchange& ex) {
Pkt4Ptr query = ex.getQuery();
Pkt4Ptr resp = ex.getResponse();
std::vector<uint8_t> requested_opts;
set<uint8_t> requested_opts;
// try to get the 'Parameter Request List' option which holds the
// codes of requested options.
OptionUint8ArrayPtr option_prl = boost::dynamic_pointer_cast<
OptionUint8Array>(query->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
// Get the codes of requested options.
// Get the list of options that client requested.
if (option_prl) {
requested_opts = option_prl->getValues();
for (uint16_t code : option_prl->getValues()) {
static_cast<void>(requested_opts.insert(code));
}
}
// Iterate on the configured option list to add persistent options
for (CfgOptionList::const_iterator copts = co_list.begin();
copts != co_list.end(); ++copts) {
const OptionContainerPtr& opts = (*copts)->getAll(DHCP4_OPTION_SPACE);
for (auto const& copts : co_list) {
const OptionContainerPtr& opts = copts->getAll(DHCP4_OPTION_SPACE);
if (!opts) {
continue;
}
@@ -1850,86 +1839,127 @@ Dhcpv4Srv::appendRequestedOptions(Dhcpv4Exchange& ex) {
desc != range.second; ++desc) {
// Add the persistent option code to requested options
if (desc->option_) {
uint8_t code = static_cast<uint8_t>(desc->option_->getType());
requested_opts.push_back(code);
static_cast<void>(requested_opts.insert(desc->option_->getType()));
}
}
}
// For each requested option code get the instance of the option
// to be returned to the client.
for (std::vector<uint8_t>::const_iterator opt = requested_opts.begin();
opt != requested_opts.end(); ++opt) {
// Add nothing when it is already there
if (!resp->getOption(*opt)) {
for (auto const& opt : requested_opts) {
// Add nothing when it is already there.
// Skip special cases: DHO_VIVSO_SUBOPTIONS
if (opt == DHO_VIVSO_SUBOPTIONS) {
continue;
}
if (!resp->getOption(opt)) {
// Iterate on the configured option list
for (CfgOptionList::const_iterator copts = co_list.begin();
copts != co_list.end(); ++copts) {
OptionDescriptor desc = (*copts)->get(DHCP4_OPTION_SPACE, *opt);
for (auto const& copts : co_list) {
OptionDescriptor desc = copts->get(DHCP4_OPTION_SPACE, opt);
// Got it: add it and jump to the outer loop
if (desc.option_) {
resp->addOption(desc.option_);
resp->Pkt::addOption(desc.option_);
break;
}
}
}
}
if (requested_opts.count(DHO_VIVSO_SUBOPTIONS) > 0) {
set<uint32_t> vendor_ids;
for (auto opt : resp->getOptions(DHO_VIVSO_SUBOPTIONS)) {
OptionVendorPtr vendor_opts;
vendor_opts = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
if (vendor_opts) {
static_cast<void>(vendor_ids.insert(vendor_opts->getVendorId()));
}
}
// Iterate on the configured option list
for (auto const& copts : co_list) {
for (OptionDescriptor desc : copts->getList(DHCP4_OPTION_SPACE,
DHO_VIVSO_SUBOPTIONS)) {
if (!desc.option_) {
continue;
}
OptionVendorPtr vendor_opts =
boost::dynamic_pointer_cast<OptionVendor>(desc.option_);
if (!vendor_opts) {
continue;
}
// Is the vendor id already in the response?
uint32_t vendor_id = vendor_opts->getVendorId();
if (vendor_ids.count(vendor_id) > 0) {
continue;
}
// Got it: add it.
resp->Pkt::addOption(desc.option_);
static_cast<void>(vendor_ids.insert(vendor_id));
}
}
}
}
void
Dhcpv4Srv::appendRequestedVendorOptions(Dhcpv4Exchange& ex) {
// Get the configured subnet suitable for the incoming packet.
Subnet4Ptr subnet = ex.getContext()->subnet_;
const CfgOptionList& co_list = ex.getCfgOptionList();
// Leave if there is no subnet matching the incoming packet.
// There is no need to log the error message here because
// it will be logged in the assignLease() when it fails to
// pick the suitable subnet. We don't want to duplicate
// error messages in such case.
if (!subnet) {
//
// Also, if there's no options to possibly assign, give up.
if (!subnet || co_list.empty()) {
return;
}
// Unlikely short cut
const CfgOptionList& co_list = ex.getCfgOptionList();
if (co_list.empty()) {
return;
}
Pkt4Ptr query = ex.getQuery();
Pkt4Ptr resp = ex.getResponse();
set<uint32_t> vendor_ids;
uint32_t vendor_id = 0;
// Try to get the vendor option from the client packet. This is how it's
// supposed to be done. Client sends vivso, we look at the vendor-id and
// then send back the vendor options specific to that client.
boost::shared_ptr<OptionVendor> vendor_req = boost::dynamic_pointer_cast<
OptionVendor>(ex.getQuery()->getOption(DHO_VIVSO_SUBOPTIONS));
if (vendor_req) {
vendor_id = vendor_req->getVendorId();
}
// Something is fishy. Client was supposed to send vivso, but didn't.
// Let's try an alternative. It's possible that the server already
// inserted vivso in the response message, (e.g. by using client
// classification or perhaps a hook inserted it).
boost::shared_ptr<OptionVendor> vendor_rsp = boost::dynamic_pointer_cast<
OptionVendor>(ex.getResponse()->getOption(DHO_VIVSO_SUBOPTIONS));
// The server could have provided the option using client classification or
// hooks. If there're vendor info options in the response already, use them.
map<uint32_t, OptionVendorPtr> vendor_rsps;
for (auto opt : resp->getOptions(DHO_VIVSO_SUBOPTIONS)) {
OptionVendorPtr vendor_rsp;
vendor_rsp = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
if (vendor_rsp) {
vendor_id = vendor_rsp->getVendorId();
uint32_t vendor_id = vendor_rsp->getVendorId();
vendor_rsps[vendor_id] = vendor_rsp;
static_cast<void>(vendor_ids.insert(vendor_id));
}
}
if (!vendor_req && !vendor_rsp) {
// Ok, we're out of luck today. Neither client nor server packets
// have vivso. There is no way to figure out vendor-id here.
// We give up.
// Next, try to get the vendor-id from the client packet's
// vendor-specific information option (17).
map<uint32_t, OptionVendorPtr> vendor_reqs;
for (auto opt : query->getOptions(DHO_VIVSO_SUBOPTIONS)) {
OptionVendorPtr vendor_req;
vendor_req = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
if (vendor_req) {
uint32_t vendor_id = vendor_req->getVendorId();
vendor_reqs[vendor_id] = vendor_req;
static_cast<void>(vendor_ids.insert(vendor_id));
}
}
// If there's no vendor option in either request or response, then there's no way
// to figure out what the vendor-id values are and we give up.
if (vendor_ids.empty()) {
return;
}
std::vector<uint8_t> requested_opts;
map<uint32_t, set<uint8_t> > requested_opts;
// Let's try to get ORO within that vendor-option.
// This is specific to vendor-id=4491 (Cable Labs). Other vendors may have
// different policies.
OptionUint8ArrayPtr oro;
if (vendor_id == VENDOR_ID_CABLE_LABS && vendor_req) {
if (vendor_reqs.count(VENDOR_ID_CABLE_LABS) > 0) {
OptionVendorPtr vendor_req = vendor_reqs[VENDOR_ID_CABLE_LABS];
OptionPtr oro_generic = vendor_req->getOption(DOCSIS3_V4_ORO);
if (oro_generic) {
// Vendor ID 4491 makes Kea look at DOCSIS3_V4_OPTION_DEFINITIONS
@@ -1937,53 +1967,61 @@ Dhcpv4Srv::appendRequestedVendorOptions(Dhcpv4Exchange& ex) {
// created as an OptionUint8Array, but might not be for other
// vendor IDs.
oro = boost::dynamic_pointer_cast<OptionUint8Array>(oro_generic);
// Get the list of options that client requested.
if (oro) {
requested_opts = oro->getValues();
}
if (oro) {
set<uint8_t> oro_req_opts;
for (uint8_t code : oro->getValues()) {
static_cast<void>(oro_req_opts.insert(code));
}
requested_opts[VENDOR_ID_CABLE_LABS] = oro_req_opts;
}
}
// Iterate on the configured option list to add persistent options
for (CfgOptionList::const_iterator copts = co_list.begin();
copts != co_list.end(); ++copts) {
const OptionContainerPtr& opts = (*copts)->getAll(vendor_id);
for (uint32_t vendor_id : vendor_ids) {
for (auto const& copts : co_list) {
const OptionContainerPtr& opts = copts->getAll(vendor_id);
if (!opts) {
continue;
}
// Get persistent options
const OptionContainerPersistIndex& idx = opts->get<2>();
const OptionContainerPersistRange& range = idx.equal_range(true);
for (OptionContainerPersistIndex::const_iterator desc = range.first;
desc != range.second; ++desc) {
if (!desc->option_) {
continue;
}
// Add the persistent option code to requested options
if (desc->option_) {
uint8_t code = static_cast<uint8_t>(desc->option_->getType());
requested_opts.push_back(code);
}
static_cast<void>(requested_opts[vendor_id].insert(desc->option_->getType()));
}
}
// If there is nothing to add don't do anything then.
if (requested_opts.empty()) {
return;
// If there is nothing to add don't do anything with this vendor.
// This will explicitly not echo back vendor options from the request
// that either correspond to a vendor not known to Kea even if the
// option encapsulates data or there are no persistent options
// configured for this vendor so Kea does not send any option back.
if (requested_opts[vendor_id].empty()) {
continue;
}
if (!vendor_rsp) {
// It's possible that vivso was inserted already by client class or
// a hook. If that is so, let's use it.
// It's possible that the vendor opts option was inserted already
// by client class or a hook. If that is so, let's use it.
OptionVendorPtr vendor_rsp;
if (vendor_rsps.count(vendor_id) > 0) {
vendor_rsp = vendor_rsps[vendor_id];
} else {
vendor_rsp.reset(new OptionVendor(Option::V4, vendor_id));
}
// Get the list of options that client requested.
bool added = false;
for (std::vector<uint8_t>::const_iterator code = requested_opts.begin();
code != requested_opts.end(); ++code) {
if (!vendor_rsp->getOption(*code)) {
for (CfgOptionList::const_iterator copts = co_list.begin();
copts != co_list.end(); ++copts) {
OptionDescriptor desc = (*copts)->get(vendor_id, *code);
for (uint8_t opt : requested_opts[vendor_id]) {
if (!vendor_rsp->getOption(opt)) {
for (auto const& copts : co_list) {
OptionDescriptor desc = copts->get(vendor_id, opt);
if (desc.option_) {
vendor_rsp->addOption(desc.option_);
added = true;
@@ -1993,10 +2031,11 @@ Dhcpv4Srv::appendRequestedVendorOptions(Dhcpv4Exchange& ex) {
}
}
// If we added some sub-options and the vivso option is not in
// If we added some sub-options and the vendor opts option is not in
// the response already, then add it.
if (added && !ex.getResponse()->getOption(DHO_VIVSO_SUBOPTIONS)) {
ex.getResponse()->addOption(vendor_rsp);
if (added && (vendor_rsps.count(vendor_id) == 0)) {
resp->Pkt::addOption(vendor_rsp);
}
}
}
@@ -2004,15 +2043,12 @@ void
Dhcpv4Srv::appendBasicOptions(Dhcpv4Exchange& ex) {
// Identify options that we always want to send to the
// client (if they are configured).
static const uint16_t required_options[] = {
static const std::vector<uint16_t> required_options = {
DHO_ROUTERS,
DHO_DOMAIN_NAME_SERVERS,
DHO_DOMAIN_NAME,
DHO_DHCP_SERVER_IDENTIFIER };
static size_t required_options_size =
sizeof(required_options) / sizeof(required_options[0]);
// Get the subnet.
Subnet4Ptr subnet = ex.getContext()->subnet_;
if (!subnet) {
@@ -2029,14 +2065,12 @@ Dhcpv4Srv::appendBasicOptions(Dhcpv4Exchange& ex) {
// Try to find all 'required' options in the outgoing
// message. Those that are not present will be added.
for (int i = 0; i < required_options_size; ++i) {
OptionPtr opt = resp->getOption(required_options[i]);
for (auto const& required : required_options) {
OptionPtr opt = resp->getOption(required);
if (!opt) {
// Check whether option has been configured.
for (CfgOptionList::const_iterator copts = co_list.begin();
copts != co_list.end(); ++copts) {
OptionDescriptor desc = (*copts)->get(DHCP4_OPTION_SPACE,
required_options[i]);
for (auto const& copts : co_list) {
OptionDescriptor desc = copts->get(DHCP4_OPTION_SPACE, required);
if (desc.option_) {
resp->addOption(desc.option_);
break;

View File

@@ -8,14 +8,16 @@
// vendor options in DHCPv4:
//
// vivso (125) - vendor independent vendor specific option. This is by far the
// most popular
// most popular.
// vendor specific (43) - this is probably the second most popular.
// Unfortunately, its definition is blurry, so there are many
// similar, but not exact implementations that do things in
// different ways.
// Unfortunately, its definition is blurry, so there are
// many similar, but not exact implementations that do
// things in different ways.
// vivco (124) - vendor independent vendor class option.
// class identifier (60) - not exactly vendor specific. It's a string, but the
// content of that string identifies what kind of vendor device
// content of that string identifies what kind of vendor
// device this is.
// client-class (77) - this specifies (as a plain string) what kind of device
// this is.
#include <config.h>
@@ -63,10 +65,28 @@ public:
/// @brief Checks if Option Request Option (ORO) in docsis (vendor-id=4491)
/// vendor options is parsed correctly and the requested options are
/// actually assigned. Also covers negative tests - that options are not
/// actually assigned. Also covers negative tests that options are not
/// provided when a different vendor ID is given.
void testVendorOptionsORO(int vendor_id) {
// Create a config with a custom option for Cable Labs.
///
/// @note Kea only knows how to process VENDOR_ID_CABLE_LABS DOCSIS3_V4_ORO
/// (suboption 1).
///
/// @param configured_vendor_ids The vendor IDs that are configured in the
/// server: 4491 or both 4491 and 3561.
/// @param requested_vendor_ids Then vendor IDs that are present in ORO.
/// @param requested_options The requested options in ORO.
void testVendorOptionsORO(std::vector<uint32_t> configured_vendor_ids,
std::vector<uint32_t> requested_vendor_ids,
std::vector<uint32_t> requested_options) {
std::vector<uint32_t> result_vendor_ids;
ASSERT_TRUE(configured_vendor_ids.size());
ASSERT_EQ(configured_vendor_ids[0], VENDOR_ID_CABLE_LABS);
for (const auto& req : requested_vendor_ids) {
if (req == VENDOR_ID_CABLE_LABS) {
result_vendor_ids.push_back(req);
}
}
// Create a config with custom options.
string config = R"(
{
"interfaces-config": {
@@ -79,6 +99,60 @@ public:
"data": "192.0.2.1, 192.0.2.2",
"name": "tftp-servers",
"space": "vendor-4491"
},
{
"code": 22,
"csv-format": true,
"data": "first",
"name": "tag",
"space": "vendor-4491"
)";
if (configured_vendor_ids.size() > 1) {
config += R"(
},
{
"code": 2,
"csv-format": true,
"data": "10.0.2.1, 10.0.2.2",
"name": "custom",
"space": "vendor-3561",
},
{
"code": 22,
"csv-format": true,
"data": "last",
"name": "special",
"space": "vendor-3561"
)";
}
config += R"(
}
],
"option-def": [
{
"code": 22,
"name": "tag",
"space": "vendor-4491",
"type": "string"
)";
if (configured_vendor_ids.size() > 1) {
config += R"(
},
{
"code": 2,
"name": "custom",
"space": "vendor-3561",
"type": "ipv4-address",
"array": true
},
{
"code": 22,
"name": "special",
"space": "vendor-3561",
"type": "string"
)";
}
config += R"(
}
],
"subnet4": [
@@ -116,10 +190,11 @@ public:
// Set interface. It is required by the server to generate server id.
dis->setIface("eth0");
dis->setIndex(ETH0_INDEX);
OptionPtr clientid = generateClientId();
dis->addOption(clientid);
// Pass it to the server and get an advertise
// Pass it to the server and get an offer
Pkt4Ptr offer = srv.processDiscover(dis);
// Check if we get a response at all.
@@ -133,10 +208,15 @@ public:
// That suboption has code 1 and is a docsis ORO option.
boost::shared_ptr<OptionUint8Array> vendor_oro(new OptionUint8Array(Option::V4,
DOCSIS3_V4_ORO));
vendor_oro->addValue(DOCSIS3_V4_TFTP_SERVERS); // Request option 2.
OptionPtr vendor(new OptionVendor(Option::V4, vendor_id));
for (auto const& option : requested_options) {
vendor_oro->addValue(option);
}
for (auto const& vendor_id : requested_vendor_ids) {
OptionVendorPtr vendor(new OptionVendor(Option::V4, vendor_id));
vendor->addOption(vendor_oro);
dis->addOption(vendor);
dis->Pkt::addOption(vendor);
}
// Need to process DHCPDISCOVER again after requesting new option.
offer = srv.processDiscover(dis);
@@ -146,18 +226,28 @@ public:
// vendor ID was provided in the request. Otherwise, check that there is
// no vendor and stop processing since the following checks are built on
// top of the now-absent options.
OptionPtr tmp = offer->getOption(DHO_VIVSO_SUBOPTIONS);
if (vendor_id != VENDOR_ID_CABLE_LABS) {
EXPECT_FALSE(tmp);
OptionCollection tmp = offer->getOptions(DHO_VIVSO_SUBOPTIONS);
ASSERT_EQ(tmp.size(), result_vendor_ids.size());
if (!result_vendor_ids.size()) {
return;
}
ASSERT_TRUE(tmp);
for (const auto& opt : tmp) {
// The response should be an OptionVendor.
boost::shared_ptr<OptionVendor> vendor_resp =
boost::dynamic_pointer_cast<OptionVendor>(tmp);
ASSERT_TRUE(vendor_resp);
OptionVendorPtr vendor_resp;
for (auto const& vendor_id : result_vendor_ids) {
vendor_resp = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
ASSERT_TRUE(vendor_resp);
if (vendor_resp->getVendorId() == vendor_id) {
break;
}
vendor_resp.reset();
}
ASSERT_TRUE(vendor_resp);
if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
for (auto const& option : requested_options) {
if (option == DOCSIS3_V4_TFTP_SERVERS) {
// Option 2 should be present.
OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
ASSERT_TRUE(docsis2);
@@ -173,6 +263,306 @@ public:
EXPECT_EQ("192.0.2.2", addrs[1].toText());
}
if (option == 22) {
// Option 22 should be present.
OptionPtr custom = vendor_resp->getOption(22);
ASSERT_TRUE(custom);
// It should be an OptionString.
OptionStringPtr tag = boost::dynamic_pointer_cast<OptionString>(custom);
ASSERT_TRUE(tag);
// Check that the provided value match the ones in configuration.
EXPECT_EQ(tag->getValue(), "first");
}
}
} else {
// If explicitly sending OptionVendor and the vendor is not
// requested, options should not be present. Kea only knows how
// to process VENDOR_ID_CABLE_LABS DOCSIS3_V4_ORO (suboption 1).
// Option 2 should not be present.
OptionPtr docsis2 = vendor_resp->getOption(2);
ASSERT_FALSE(docsis2);
// Option 22 should not be present.
OptionPtr custom = vendor_resp->getOption(22);
ASSERT_FALSE(custom);
}
}
}
/// @brief Checks if Option Request Option (ORO) in docsis (vendor-id=4491)
/// vendor options is parsed correctly and the configured options are
/// actually assigned.
///
/// @param configured_vendor_ids The vendor IDs that are configured in the
/// server: 4491 or both 4491 and 3561.
/// @param requested_vendor_ids Then vendor IDs that are present in ORO.
/// @param requested_options The requested options in ORO.
/// @param add_vendor_option The flag which indicates if the request should
/// contain a OptionVendor option or should the server always send all the
/// OptionVendor options and suboptions.
void testVendorOptionsPersistent(std::vector<uint32_t> configured_vendor_ids,
std::vector<uint32_t> requested_vendor_ids,
std::vector<uint32_t> configured_options,
bool add_vendor_option) {
std::vector<uint32_t> result_vendor_ids;
ASSERT_TRUE(configured_vendor_ids.size());
ASSERT_EQ(configured_vendor_ids[0], VENDOR_ID_CABLE_LABS);
if (add_vendor_option) {
for (const auto& req : requested_vendor_ids) {
if (std::find(configured_vendor_ids.begin(), configured_vendor_ids.end(), req) != configured_vendor_ids.end()) {
result_vendor_ids.push_back(req);
}
}
} else {
result_vendor_ids = configured_vendor_ids;
}
ASSERT_TRUE(configured_options.size());
ASSERT_EQ(configured_options[0], DOCSIS3_V4_TFTP_SERVERS);
// Create a config with a custom options.
string config = R"(
{
"interfaces-config": {
"interfaces": [ "*" ]
},
"option-data": [
{
"always-send": true,
"code": 2,
"csv-format": true,
"data": "192.0.2.1, 192.0.2.2",
"name": "tftp-servers",
"space": "vendor-4491"
)";
if (configured_options.size() > 1) {
config += R"(
},
{
"always-send": true,
"code": 22,
"csv-format": true,
"data": "first",
"name": "tag",
"space": "vendor-4491"
)";
}
if (!add_vendor_option) {
config += R"(
},
{
"always-send": true,
"name": "vivso-suboptions",
"data": "4491",
"space": "dhcp4"
)";
}
if (configured_vendor_ids.size() > 1) {
config += R"(
},
{
"always-send": true,
"code": 2,
"csv-format": true,
"data": "10.0.2.1, 10.0.2.2",
"name": "custom",
"space": "vendor-3561"
)";
if (configured_options.size() > 1) {
config += R"(
},
{
"always-send": true,
"code": 22,
"csv-format": true,
"data": "last",
"name": "special",
"space": "vendor-3561"
)";
}
if (!add_vendor_option) {
config += R"(
},
{
"always-send": true,
"name": "vivso-suboptions",
"data": "3561",
"space": "dhcp4"
)";
}
}
config += R"(
}
],
"option-def": [
{
"code": 22,
"name": "tag",
"space": "vendor-4491",
"type": "string"
)";
if (configured_vendor_ids.size() > 1) {
config += R"(
},
{
"code": 2,
"name": "custom",
"space": "vendor-3561",
"type": "ipv4-address",
"array": true
},
{
"code": 22,
"name": "special",
"space": "vendor-3561",
"type": "string"
)";
}
config += R"(
}
],
"subnet4": [
{
"interface": "eth0",
"pools": [
{
"pool": "192.0.2.0/25"
}
],
"subnet": "192.0.2.0/24"
}
]
}
)";
// Parse the configuration.
ConstElementPtr json;
ASSERT_NO_THROW(json = parseDHCP4(config));
// Configure a mocked server.
NakedDhcpv4Srv srv(0);
ConstElementPtr x;
EXPECT_NO_THROW(x = configureDhcp4Server(srv, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(0, rcode_);
CfgMgr::instance().commit();
// Set the giaddr and hops to non-zero address as if it was relayed.
boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
dis->setGiaddr(IOAddress("192.0.2.1"));
dis->setHops(1);
// Set interface. It is required by the server to generate server id.
dis->setIface("eth0");
dis->setIndex(ETH0_INDEX);
OptionPtr clientid = generateClientId();
dis->addOption(clientid);
if (add_vendor_option) {
for (auto const& vendor_id : requested_vendor_ids) {
// Let's add a vendor-option (vendor-id=4491).
OptionVendorPtr vendor(new OptionVendor(Option::V4, vendor_id));
dis->Pkt::addOption(vendor);
}
}
// Pass it to the server and get an offer
Pkt4Ptr offer = srv.processDiscover(dis);
// check if we get response at all
ASSERT_TRUE(offer);
// Check if there is a vendor option response
OptionCollection tmp = offer->getOptions(DHO_VIVSO_SUBOPTIONS);
ASSERT_EQ(tmp.size(), result_vendor_ids.size());
for (const auto& opt : tmp) {
// The response should be an OptionVendor.
OptionVendorPtr vendor_resp;
for (auto const& vendor_id : result_vendor_ids) {
vendor_resp = boost::dynamic_pointer_cast<OptionVendor>(opt.second);
ASSERT_TRUE(vendor_resp);
if (vendor_resp->getVendorId() == vendor_id) {
break;
}
}
ASSERT_TRUE(vendor_resp);
for (auto const& option : configured_options) {
if (add_vendor_option &&
std::find(requested_vendor_ids.begin(), requested_vendor_ids.end(),
vendor_resp->getVendorId()) == requested_vendor_ids.end()) {
// If explicitly sending OptionVendor and the vendor is not
// requested, options should not be present.
if (option == DOCSIS3_V4_TFTP_SERVERS) {
// Option 2 should not be present.
OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
ASSERT_FALSE(docsis2);
}
if (option == 22) {
// Option 22 should not be present.
OptionPtr custom = vendor_resp->getOption(22);
ASSERT_FALSE(custom);
}
} else {
if (option == DOCSIS3_V4_TFTP_SERVERS) {
// Option 2 should be present.
OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
ASSERT_TRUE(docsis2);
// It should be an Option4AddrLst.
Option4AddrLstPtr tftp_srvs;
if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
tftp_srvs = boost::dynamic_pointer_cast<Option4AddrLst>(docsis2);
} else {
// The option is serialized as Option so it needs to be converted to
// Option4AddrLst.
auto const& buffer = docsis2->toBinary();
tftp_srvs.reset(new Option4AddrLst(DOCSIS3_V4_TFTP_SERVERS, buffer.begin(), buffer.end()));
}
ASSERT_TRUE(tftp_srvs);
// Check that the provided addresses match the ones in configuration.
Option4AddrLst::AddressContainer addrs = tftp_srvs->getAddresses();
ASSERT_EQ(2, addrs.size());
if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
EXPECT_EQ("192.0.2.1", addrs[0].toText());
EXPECT_EQ("192.0.2.2", addrs[1].toText());
} else {
EXPECT_EQ("10.0.2.1", addrs[0].toText());
EXPECT_EQ("10.0.2.2", addrs[1].toText());
}
}
if (option == 22) {
// Option 22 should be present.
OptionPtr custom = vendor_resp->getOption(22);
ASSERT_TRUE(custom);
// It should be an OptionString.
// The option is serialized as Option so it needs to be converted to
// OptionString.
auto const& buffer = custom->toBinary();
OptionStringPtr tag(new OptionString(Option::V4, 22, buffer.begin(), buffer.end()));
ASSERT_TRUE(tag);
// Check that the provided value match the ones in configuration.
if (vendor_resp->getVendorId() == VENDOR_ID_CABLE_LABS) {
EXPECT_EQ(tag->getValue(), "first");
} else {
EXPECT_EQ(tag->getValue(), "last");
}
}
}
}
}
}
std::unique_ptr<IfaceMgrTestConfig> iface_mgr_test_config_;
};
@@ -240,9 +630,9 @@ TEST_F(VendorOptsTest, vendorOptionsDocsis) {
ASSERT_TRUE(vendor_opt_response);
// Check if it's of a correct type
boost::shared_ptr<OptionVendor> vendor_opt =
boost::dynamic_pointer_cast<OptionVendor>(vendor_opt_response);
OptionVendorPtr vendor_opt = boost::dynamic_pointer_cast<OptionVendor>(vendor_opt_response);
ASSERT_TRUE(vendor_opt);
ASSERT_EQ(vendor_opt->getVendorId(), VENDOR_ID_CABLE_LABS);
// Get Relay Agent Info from response...
OptionPtr tftp_servers_generic = vendor_opt->getOption(DOCSIS3_V4_TFTP_SERVERS);
@@ -269,8 +659,9 @@ TEST_F(VendorOptsTest, docsisVendorOptionsParse) {
OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS);
ASSERT_TRUE(opt);
boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
ASSERT_TRUE(vendor);
ASSERT_EQ(vendor->getVendorId(), VENDOR_ID_CABLE_LABS);
// This particular capture that we have included options 1 and 5
EXPECT_TRUE(vendor->getOption(1));
@@ -287,14 +678,15 @@ TEST_F(VendorOptsTest, docsisVendorORO) {
// Let's get a traffic capture from DOCSIS3.0 modem
Pkt4Ptr dis = PktCaptures::captureRelayedDiscover();
EXPECT_NO_THROW(dis->unpack());
ASSERT_NO_THROW(dis->unpack());
// Check if the packet contains vendor specific information option
OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS);
ASSERT_TRUE(opt);
boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
ASSERT_TRUE(vendor);
ASSERT_EQ(vendor->getVendorId(), VENDOR_ID_CABLE_LABS);
opt = vendor->getOption(DOCSIS3_V4_ORO);
ASSERT_TRUE(opt);
@@ -305,90 +697,134 @@ TEST_F(VendorOptsTest, docsisVendorORO) {
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and the requested options are actually assigned.
TEST_F(VendorOptsTest, vendorOptionsORO) {
testVendorOptionsORO(VENDOR_ID_CABLE_LABS);
TEST_F(VendorOptsTest, vendorOptionsOROOneOption) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS});
}
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and the requested options are actually assigned.
TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptions) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS, 22});
}
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and the requested options are actually assigned.
TEST_F(VendorOptsTest, vendorOptionsOROOneOptionMultipleVendorsMatchOne) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS});
}
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and the requested options are actually assigned.
TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptionsMultipleVendorsMatchOne) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS, 22});
}
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and the requested options are actually assigned.
TEST_F(VendorOptsTest, vendorOptionsOROOneOptionMultipleVendorsMatchAll) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS, 3561}, {DOCSIS3_V4_TFTP_SERVERS});
}
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and the requested options are actually assigned.
TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptionsMultipleVendorsMatchAll) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS, 3561}, {DOCSIS3_V4_TFTP_SERVERS, 22});
}
// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
// provided and vendor options are expected to not be present in the response.
TEST_F(VendorOptsTest, vendorOptionsORODifferentVendorID) {
testVendorOptionsORO(32768);
TEST_F(VendorOptsTest, vendorOptionsOROOneOptionDifferentVendorID) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS}, {32768}, {DOCSIS3_V4_TFTP_SERVERS});
}
// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
// provided and vendor options are expected to not be present in the response.
TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptionsDifferentVendorID) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS}, {32768}, {DOCSIS3_V4_TFTP_SERVERS, 22});
}
// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
// provided and vendor options are expected to not be present in the response.
TEST_F(VendorOptsTest, vendorOptionsOROOneOptionDifferentVendorIDMultipleVendorsMatchNone) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS, 3561}, {32768, 16384}, {DOCSIS3_V4_TFTP_SERVERS});
}
// Same as vendorOptionsORO except a different vendor ID than Cable Labs is
// provided and vendor options are expected to not be present in the response.
TEST_F(VendorOptsTest, vendorOptionsOROMultipleOptionDifferentVendorIDMultipleVendorsMatchNone) {
testVendorOptionsORO({VENDOR_ID_CABLE_LABS, 3561}, {32768, 16384}, {DOCSIS3_V4_TFTP_SERVERS, 22});
}
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptions) {
NakedDhcpv4Srv srv(0);
TEST_F(VendorOptsTest, vendorPersistentOptionsOneOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS}, false);
}
ConstElementPtr x;
string config = "{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
"},"
" \"option-data\": [ {"
" \"name\": \"tftp-servers\","
" \"space\": \"vendor-4491\","
" \"code\": 2,"
" \"data\": \"192.0.2.1, 192.0.2.2\","
" \"csv-format\": true,"
" \"always-send\": true"
" }],"
"\"subnet4\": [ { "
" \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ],"
" \"subnet\": \"192.0.2.0/24\", "
" \"interface\": \"eth0\" "
" } ]"
"}";
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS, 22}, false);
}
ConstElementPtr json;
ASSERT_NO_THROW(json = parseDHCP4(config));
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchOne) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS}, false);
}
EXPECT_NO_THROW(x = configureDhcp4Server(srv, json));
ASSERT_TRUE(x);
comment_ = parseAnswer(rcode_, x);
ASSERT_EQ(0, rcode_);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchOne) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS, 22}, false);
}
CfgMgr::instance().commit();
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchAll) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS, 3561}, {DOCSIS3_V4_TFTP_SERVERS}, false);
}
boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
// Set the giaddr and hops to non-zero address as if it was relayed.
dis->setGiaddr(IOAddress("192.0.2.1"));
dis->setHops(1);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchAll) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS, 3561}, {DOCSIS3_V4_TFTP_SERVERS, 22}, false);
}
OptionPtr clientid = generateClientId();
dis->addOption(clientid);
// Set interface. It is required by the server to generate server id.
dis->setIface("eth0");
dis->setIndex(ETH0_INDEX);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionAddVendorOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS}, true);
}
// Let's add a vendor-option (vendor-id=4491).
OptionPtr vendor(new OptionVendor(Option::V4, 4491));
dis->addOption(vendor);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionAddVendorOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS, 22}, true);
}
// Pass it to the server and get an advertise
Pkt4Ptr offer = srv.processDiscover(dis);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchOneAddVendorOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS}, true);
}
// check if we get response at all
ASSERT_TRUE(offer);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchOneAddVendorOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS}, {DOCSIS3_V4_TFTP_SERVERS, 22}, true);
}
// Check if there is a vendor option response
OptionPtr tmp = offer->getOption(DHO_VIVSO_SUBOPTIONS);
ASSERT_TRUE(tmp);
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsOneOptionMultipleVendorsMatchAllAddVendorOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS, 3561}, {DOCSIS3_V4_TFTP_SERVERS}, true);
}
// The response should be OptionVendor object
boost::shared_ptr<OptionVendor> vendor_resp =
boost::dynamic_pointer_cast<OptionVendor>(tmp);
ASSERT_TRUE(vendor_resp);
OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
ASSERT_TRUE(docsis2);
Option4AddrLstPtr tftp_srvs = boost::dynamic_pointer_cast<Option4AddrLst>(docsis2);
ASSERT_TRUE(tftp_srvs);
Option4AddrLst::AddressContainer addrs = tftp_srvs->getAddresses();
ASSERT_EQ(2, addrs.size());
EXPECT_EQ("192.0.2.1", addrs[0].toText());
EXPECT_EQ("192.0.2.2", addrs[1].toText());
// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
// vendor options is parsed correctly and persistent options are actually assigned.
TEST_F(VendorOptsTest, vendorPersistentOptionsMultipleOptionMultipleVendorsMatchAllAddVendorOption) {
testVendorOptionsPersistent({VENDOR_ID_CABLE_LABS, 3561}, {VENDOR_ID_CABLE_LABS, 3561}, {DOCSIS3_V4_TFTP_SERVERS, 22}, true);
}
// Test checks whether it is possible to use option definitions defined in
@@ -540,7 +976,7 @@ TEST_F(VendorOptsTest, vivsoInResponseOnly) {
// Check that it includes vivso with vendor-id = 25167
OptionVendorPtr rsp_vivso = boost::dynamic_pointer_cast<OptionVendor>(rsp);
ASSERT_TRUE(rsp_vivso);
EXPECT_EQ(25167, rsp_vivso->getVendorId());
EXPECT_EQ(rsp_vivso->getVendorId(), 25167);
// Now check that it contains suboption 2 with appropriate content.
OptionPtr subopt2 = rsp_vivso->getOption(2);

View File

@@ -207,8 +207,8 @@ TEST_F(HAImplTest, buffer4Receive) {
};
// Provide DHCP message type option, truncated vendor option and domain name.
// Parsing this message should be successful but domain name following the
// truncated vendor option should be skipped.
// Parsing this message should be successful but vendor option should be
// skipped.
std::vector<uint8_t> options = {
53, 1, 1, // Message type = DHCPDISCOVER
125, 6, // vendor options
@@ -240,8 +240,10 @@ TEST_F(HAImplTest, buffer4Receive) {
// Check that the message has been parsed. The DHCP message type should
// be set in this case.
EXPECT_EQ(DHCPDISCOVER, static_cast<int>(query4->getType()));
// Domain name should be skipped because the vendor option was truncated.
EXPECT_FALSE(query4->getOption(DHO_DOMAIN_NAME));
// Vendor option should be skipped because it was truncated.
EXPECT_FALSE(query4->getOption(DHO_VIVSO_SUBOPTIONS));
// Domain name should not be skipped because the vendor option was truncated.
EXPECT_TRUE(query4->getOption(DHO_DOMAIN_NAME));
}
// Tests for buffer6_receive callout implementation.

View File

@@ -304,7 +304,6 @@ LibDHCP::optionFactory(Option::Universe u,
return (it->second(u, type, buf));
}
size_t
LibDHCP::unpackOptions6(const OptionBuffer& buf,
const std::string& option_space,
@@ -576,6 +575,10 @@ LibDHCP::unpackOptions4(const OptionBuffer& buf,
}
}
if (space_is_dhcp4 && opt_type == DHO_VIVSO_SUBOPTIONS) {
num_defs = 0;
}
OptionPtr opt;
if (num_defs > 1) {
// Multiple options of the same code are not supported right now!
@@ -682,6 +685,46 @@ LibDHCP::fuseOptions4(OptionCollection& options) {
return (result);
}
void
LibDHCP::extendVendorOptions4(isc::dhcp::OptionCollection& options) {
map<uint32_t, OptionCollection> vendors_data;
for (auto const& option : options) {
if (option.second->getType() == DHO_VIVSO_SUBOPTIONS) {
uint32_t offset = 0;
auto const& data = option.second->getData();
auto const& begin = data.begin();
auto const& end = data.end();
size_t size;
while ((size = distance(begin + offset, end)) != 0) {
if (size < sizeof(uint32_t)) {
options.erase(DHO_VIVSO_SUBOPTIONS);
isc_throw(SkipRemainingOptionsError,
"Truncated vendor-specific information option"
<< ", length=" << size);
}
uint32_t vendor_id = isc::util::readUint32(&(*begin) + offset, distance(begin, end));
offset += 4;
OptionBuffer vendor_buffer(begin + offset, end);
try {
offset += LibDHCP::unpackVendorOptions4(vendor_id, vendor_buffer, vendors_data[vendor_id]);
} catch (const SkipThisOptionError&) {
} catch (const Exception&) {
options.erase(DHO_VIVSO_SUBOPTIONS);
throw;
}
}
}
}
options.erase(DHO_VIVSO_SUBOPTIONS);
for (auto const& vendor : vendors_data) {
OptionVendorPtr vendor_opt(new OptionVendor(Option::V4, vendor.first));
for (auto const& option : vendor.second) {
vendor_opt->addOption(option.second);
}
options.insert(std::make_pair(DHO_VIVSO_SUBOPTIONS, vendor_opt));
}
}
size_t
LibDHCP::unpackVendorOptions6(const uint32_t vendor_id,
const OptionBuffer& buf,

View File

@@ -299,6 +299,13 @@ public:
/// @return True if any option has been fused, false otherwise.
static bool fuseOptions4(isc::dhcp::OptionCollection& options);
/// @brief Extend vendor options from fused options in multiple OptionVendor
/// options and add respective suboptions.
///
/// @param options The option container which needs to be updated with
/// extended vendor options.
static void extendVendorOptions4(isc::dhcp::OptionCollection& options);
/// @brief Parses provided buffer as DHCPv4 options and creates
/// Option objects.
///

View File

@@ -16,19 +16,22 @@ using namespace isc::dhcp;
OptionVendor::OptionVendor(Option::Universe u, const uint32_t vendor_id)
: Option(u, u == Option::V4 ?
static_cast<uint16_t>(DHO_VIVSO_SUBOPTIONS) :
static_cast<uint16_t>(D6O_VENDOR_OPTS)),
vendor_id_(vendor_id) {
static_cast<uint16_t>(D6O_VENDOR_OPTS)), vendor_id_(vendor_id) {
}
OptionVendor::OptionVendor(Option::Universe u, OptionBufferConstIter begin,
OptionBufferConstIter end)
: Option(u, u == Option::V4?
static_cast<uint16_t>(DHO_VIVSO_SUBOPTIONS) :
static_cast<uint16_t>(D6O_VENDOR_OPTS)),
vendor_id_(0) {
static_cast<uint16_t>(D6O_VENDOR_OPTS)), vendor_id_(0) {
unpack(begin, end);
}
OptionVendor::OptionVendor(const OptionVendor& option) : Option(option),
vendor_id_(option.vendor_id_) {
Option::operator=(option);
}
OptionPtr
OptionVendor::clone() const {
return (cloneInternal<OptionVendor>());
@@ -45,7 +48,12 @@ void OptionVendor::pack(isc::util::OutputBuffer& buf, bool check) const {
// Calculate and store data-len as follows:
// data-len = total option length - header length
// - enterprise id field length - data-len field size
buf.writeUint8(dataLen());
// length of all suboptions
uint8_t length = 0;
for (auto const& opt : options_) {
length += opt.second->len();
}
buf.writeUint8(length);
}
packOptions(buf, check);
@@ -53,7 +61,6 @@ void OptionVendor::pack(isc::util::OutputBuffer& buf, bool check) const {
void OptionVendor::unpack(OptionBufferConstIter begin,
OptionBufferConstIter end) {
// We throw SkipRemainingOptionsError so callers can
// abandon further unpacking, if desired.
if (distance(begin, end) < sizeof(uint32_t)) {
@@ -62,6 +69,8 @@ void OptionVendor::unpack(OptionBufferConstIter begin,
<< ", length=" << distance(begin, end));
}
options_.clear();
vendor_id_ = isc::util::readUint32(&(*begin), distance(begin, end));
OptionBuffer vendor_buffer(begin + 4, end);
@@ -84,31 +93,26 @@ uint16_t OptionVendor::len() const {
}
// length of all suboptions
for (OptionCollection::const_iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
for (auto const& opt : options_) {
length += opt.second->len();
}
return (length);
}
uint8_t
OptionVendor::dataLen() const {
// Calculate and store data-len as follows:
// data-len = total option length - header length
// - enterprise id field length - data-len field size
return (len() - getHeaderLen() - sizeof(uint32_t) - sizeof(uint8_t));
}
std::string
OptionVendor::toText(int indent) const {
std::stringstream output;
output << headerToText(indent) << ": "
<< getVendorId() << " (uint32)";
output << headerToText(indent) << ": ";
output << vendor_id_ << " (uint32)";
// For the DHCPv4 there is one more field.
if (getUniverse() == Option::V4) {
output << " " << static_cast<int>(dataLen()) << " (uint8)";
uint32_t length = 0;
for (auto const& opt : options_) {
length += opt.second->len();
}
output << " " << length << " (uint8)";
}
// Append suboptions.
@@ -116,3 +120,11 @@ OptionVendor::toText(int indent) const {
return (output.str());
}
OptionVendor& OptionVendor::operator=(const OptionVendor& rhs) {
if (&rhs != this) {
Option::operator=(rhs);
vendor_id_ = rhs.vendor_id_;
}
return (*this);
}

View File

@@ -50,6 +50,14 @@ public:
OptionVendor(Option::Universe u, OptionBufferConstIter begin,
OptionBufferConstIter end);
/// @brief Copy constructor.
///
/// This constructor makes a deep copy of the option and all of the
/// suboptions. It calls @ref getOptionsCopy to deep copy suboptions.
///
/// @param source Option to be copied.
OptionVendor(const OptionVendor& option);
/// @brief Copies this option and returns a pointer to the copy.
OptionPtr clone() const;
@@ -77,12 +85,16 @@ public:
/// @brief Sets enterprise identifier
///
/// @param vendor_id vendor identifier
void setVendorId(const uint32_t vendor_id) { vendor_id_ = vendor_id; }
void setVendorId(const uint32_t vendor_id) {
vendor_id_ = vendor_id;
}
/// @brief Returns enterprise identifier
///
/// @return enterprise identifier
uint32_t getVendorId() const { return (vendor_id_); }
uint32_t getVendorId() const {
return (vendor_id_);
}
/// @brief returns complete length of option
///
@@ -98,19 +110,19 @@ public:
/// @return Vendor option in the textual format.
virtual std::string toText(int indent = 0) const;
/// @brief Assignment operator.
///
/// The assignment operator performs a deep copy of the option and
/// its suboptions. It calls @ref getOptionsCopy to deep copy
/// suboptions.
///
/// @param rhs Option to be assigned.
OptionVendor& operator=(const OptionVendor& rhs);
private:
/// @brief Calculates the data-len value for DHCPv4.
///
/// The data-len field is only present in DHCPv4 space. It follows
/// the vendor-id field. This method is called from the
/// @c OptionVendor::pack and @c OptionVendor::toText to calculate
/// this value.
///
/// @return Returns calculated data-len value.
uint8_t dataLen() const;
uint32_t vendor_id_; ///< Enterprise-id
/// @brief Enterprise-id
uint32_t vendor_id_;
};
/// Pointer to a vendor option

View File

@@ -227,6 +227,10 @@ Pkt4::unpack() {
// access the entire data.
LibDHCP::fuseOptions4(options_);
// Kea supports multiple vendor options so it needs to split received and
// fused options in multiple OptionVendor instances.
LibDHCP::extendVendorOptions4(options_);
// No need to call check() here. There are thorough tests for this
// later (see Dhcp4Srv::accept()). We want to drop the packet later,
// so we'll be able to log more detailed drop reason.

View File

@@ -408,7 +408,7 @@ TEST_F(LibDhcpTest, packOptions6) {
OptionBuffer(v6packed + 46, v6packed + 50)));
boost::shared_ptr<OptionInt<uint32_t> >
vsi(new OptionInt<uint32_t>(Option::V6, D6O_VENDOR_OPTS, 4491));
vsi(new OptionInt<uint32_t>(Option::V6, D6O_VENDOR_OPTS, VENDOR_ID_CABLE_LABS));
vsi->addOption(cm_mac);
vsi->addOption(cmts_caps);
@@ -510,15 +510,19 @@ TEST_F(LibDhcpTest, unpackOptions6) {
EXPECT_EQ(D6O_VENDOR_OPTS, x->second->getType());
EXPECT_EQ(26, x->second->len());
OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(x->second);
ASSERT_TRUE(vendor);
ASSERT_EQ(vendor->getVendorId(), VENDOR_ID_CABLE_LABS);
// CM MAC Address Option
OptionPtr cm_mac = x->second->getOption(OPTION_CM_MAC);
OptionPtr cm_mac = vendor->getOption(OPTION_CM_MAC);
ASSERT_TRUE(cm_mac);
EXPECT_EQ(OPTION_CM_MAC, cm_mac->getType());
ASSERT_EQ(10, cm_mac->len());
EXPECT_EQ(0, memcmp(&cm_mac->getData()[0], v6packed + 54, 6));
// CMTS Capabilities
OptionPtr cmts_caps = x->second->getOption(OPTION_CMTS_CAPS);
OptionPtr cmts_caps = vendor->getOption(OPTION_CMTS_CAPS);
ASSERT_TRUE(cmts_caps);
EXPECT_EQ(OPTION_CMTS_CAPS, cmts_caps->getType());
ASSERT_EQ(8, cmts_caps->len());
@@ -1076,6 +1080,72 @@ TEST_F(LibDhcpTest, fuseLongOptionWithLongSuboption) {
}
}
TEST_F(LibDhcpTest, extentVendorOptions4) {
OptionPtr suboption;
OptionVendorPtr opt1(new OptionVendor(Option::V4, 1));
suboption.reset(new OptionString(Option::V4, 16, "first"));
opt1->addOption(suboption);
OptionVendorPtr opt2(new OptionVendor(Option::V4, 1));
suboption.reset(new OptionString(Option::V4, 32, "second"));
opt2->addOption(suboption);
OptionVendorPtr opt3(new OptionVendor(Option::V4, 2));
suboption.reset(new OptionString(Option::V4, 128, "extra"));
opt3->addOption(suboption);
OptionVendorPtr opt4(new OptionVendor(Option::V4, 1));
suboption.reset(new OptionString(Option::V4, 64, "third"));
opt4->addOption(suboption);
OptionCollection container;
container.insert(make_pair(1, opt1));
container.insert(make_pair(1, opt2));
OptionCollection options;
for (auto const& option : container) {
const OptionBuffer& buffer = option.second->toBinary();
options.insert(make_pair(option.second->getType(),
OptionPtr(new Option(Option::V4,
option.second->getType(),
buffer))));
}
ASSERT_NO_THROW(LibDHCP::fuseOptions4(options));
ASSERT_EQ(options.size(), 1);
container.clear();
container.insert(make_pair(1, options.begin()->second));
container.insert(make_pair(2, opt3));
container.insert(make_pair(1, opt4));
ASSERT_EQ(container.size(), 3);
options.clear();
for (auto const& option : container) {
const OptionBuffer& buffer = option.second->toBinary();
options.insert(make_pair(option.second->getType(),
OptionPtr(new Option(Option::V4,
option.second->getType(),
buffer))));
}
ASSERT_EQ(options.size(), 3);
LibDHCP::extendVendorOptions4(options);
ASSERT_EQ(options.size(), 2);
for (auto const& option : options) {
OptionCollection suboptions = option.second->getOptions();
OptionPtr suboption;
OptionVendorPtr vendor = boost::dynamic_pointer_cast<OptionVendor>(option.second);
ASSERT_TRUE(vendor);
if (vendor->getVendorId() == 1) {
ASSERT_EQ(suboptions.size(), 3);
suboption = option.second->getOption(16);
ASSERT_TRUE(suboption);
suboption = option.second->getOption(32);
ASSERT_TRUE(suboption);
suboption = option.second->getOption(64);
ASSERT_TRUE(suboption);
} else if (vendor->getVendorId() == 2) {
ASSERT_EQ(suboptions.size(), 1);
suboption = option.second->getOption(128);
ASSERT_TRUE(suboption);
} else {
FAIL() << "unexpected vendor type: " << vendor->getVendorId();
}
}
}
// This test verifies that pack options for v4 is working correctly.
TEST_F(LibDhcpTest, packOptions4) {
@@ -1094,7 +1164,7 @@ TEST_F(LibDhcpTest, packOptions4) {
OptionPtr opt5(new Option(Option::V4, 128, payload[4]));
// Create vendor option instance with DOCSIS3.0 enterprise id.
OptionVendorPtr vivsi(new OptionVendor(Option::V4, 4491));
OptionVendorPtr vivsi(new OptionVendor(Option::V4, VENDOR_ID_CABLE_LABS));
vivsi->addOption(OptionPtr(new Option4AddrLst(DOCSIS3_V4_TFTP_SERVERS,
IOAddress("10.0.0.10"))));
@@ -1199,6 +1269,8 @@ TEST_F(LibDhcpTest, unpackOptions4) {
deferred, false);
);
ASSERT_NO_THROW(LibDHCP::extendVendorOptions4(options));
isc::dhcp::OptionCollection::const_iterator x = options.find(12);
ASSERT_FALSE(x == options.end()); // option 1 should exist
// Option 12 holds a string so let's cast it to an appropriate type.
@@ -1245,7 +1317,7 @@ TEST_F(LibDhcpTest, unpackOptions4) {
OptionVendorPtr vivsi = boost::dynamic_pointer_cast<OptionVendor>(x->second);
ASSERT_TRUE(vivsi);
EXPECT_EQ(DHO_VIVSO_SUBOPTIONS, vivsi->getType());
EXPECT_EQ(4491, vivsi->getVendorId());
EXPECT_EQ(VENDOR_ID_CABLE_LABS, vivsi->getVendorId());
OptionCollection suboptions = vivsi->getOptions();
// There should be one suboption of V-I VSI.
@@ -2094,7 +2166,6 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
LibDhcpTest::testStdOptionDefs4(DHO_VIVCO_SUBOPTIONS, vivco_buf.begin(),
vivco_buf.end(), typeid(OptionVendorClass));
LibDhcpTest::testStdOptionDefs4(DHO_VIVSO_SUBOPTIONS, vivsio_buf.begin(),
vivsio_buf.end(), typeid(OptionVendor));
@@ -2723,7 +2794,7 @@ TEST_F(LibDhcpTest, vendorClass6) {
// Let's investigate if the option content is correct
// 3 fields expected: vendor-id, data-len and data
EXPECT_EQ(4491, vclass->getVendorId());
EXPECT_EQ(VENDOR_ID_CABLE_LABS, vclass->getVendorId());
EXPECT_EQ(20, vclass->len());
ASSERT_EQ(1, vclass->getTuplesNum());
EXPECT_EQ("eRouter1.0", vclass->getTuple(0).getText());