diff options
Diffstat (limited to 'src/bin/dhcp6/tests/vendor_opts_unittest.cc')
-rw-r--r-- | src/bin/dhcp6/tests/vendor_opts_unittest.cc | 639 |
1 files changed, 639 insertions, 0 deletions
diff --git a/src/bin/dhcp6/tests/vendor_opts_unittest.cc b/src/bin/dhcp6/tests/vendor_opts_unittest.cc new file mode 100644 index 0000000..51bb4b5 --- /dev/null +++ b/src/bin/dhcp6/tests/vendor_opts_unittest.cc @@ -0,0 +1,639 @@ +// Copyright (C) 2019-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// This file is dedicated to testing vendor options in DHCPv6. There +// are several related options: +// +// client-class (15) - this specifies (as a plain string) what kind of +// device this is +// vendor-class (16) - contains an enterprise-id followed by zero or +// more of vendor-class data. +// vendor-option (17) - contains an enterprise-id followed by zero or +// more vendor suboptions + +#include <config.h> + +#include <asiolink/io_address.h> +#include <dhcp/dhcp6.h> +#include <dhcp/option_vendor.h> +#include <dhcp/option_vendor_class.h> +#include <dhcp6/tests/dhcp6_test_utils.h> +#include <dhcp6/tests/dhcp6_client.h> +#include <dhcp6/json_config_parser.h> +#include <dhcp/tests/pkt_captures.h> +#include <dhcp/docsis3_option_defs.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcp/option_string.h> +#include <cc/command_interpreter.h> + +#include <gtest/gtest.h> + +#include <string> +#include <vector> + +using namespace isc; +using namespace isc::config; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::asiolink; + +/// @brief Class dedicated to testing vendor options in DHCPv6 +class VendorOptsTest : public Dhcpv6SrvTest { +public: + /// @brief Called before each test + void SetUp() override { + iface_mgr_test_config_.reset(new IfaceMgrTestConfig(true)); + IfaceMgr::instance().openSockets6(); + } + + /// @brief Called after each test + void TearDown() override { + iface_mgr_test_config_.reset(); + IfaceMgr::instance().closeSockets(); + } + + void testVendorOptionsORO(int vendor_id) { + // Create a config with a custom option for Cable Labs. + string config = R"( + { + "interfaces-config": { + "interfaces": [ "*" ] + }, + "option-data": [ + { + "data": "normal_erouter_v6.cm", + "name": "config-file", + "space": "vendor-4491" + } + ], + "option-def": [ + { + "code": 33, + "name": "config-file", + "space": "vendor-4491", + "type": "string" + } + ], + "preferred-lifetime": 3000, + "rebind-timer": 2000, + "renew-timer": 1000, + "subnet6": [ + { + "interface": "eth0", + "interface-id": "", + "pools": [ + { + "pool": "2001:db8:1::/64" + } + ], + "preferred-lifetime": 3000, + "rebind-timer": 1000, + "renew-timer": 1000, + "subnet": "2001:db8:1::/48", + "valid-lifetime": 4000 + } + ], + "valid-lifetime": 4000 + } + )"; + + // Configure a mocked server. + ASSERT_NO_THROW(configure(config)); + + // Create a SOLICIT. + Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + sol->setRemoteAddr(IOAddress("fe80::abcd")); + sol->setIface("eth0"); + sol->setIndex(ETH0_INDEX); + sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + OptionPtr clientid = generateClientId(); + sol->addOption(clientid); + + // Pass it to the server and get an advertise. + AllocEngine::ClientContext6 ctx; + bool drop = !srv_.earlyGHRLookup(sol, ctx); + ASSERT_FALSE(drop); + srv_.initContext(sol, ctx, drop); + ASSERT_FALSE(drop); + Pkt6Ptr adv = srv_.processSolicit(ctx); + + // Check if we get a response at all. + ASSERT_TRUE(adv); + + // We did not include any vendor opts in SOLICIT, so there should be none + // in ADVERTISE. + ASSERT_FALSE(adv->getOption(D6O_VENDOR_OPTS)); + + // Let's add a vendor-option (vendor-id=4491) with a single sub-option. + // That suboption has code 1 and is a docsis ORO option. + boost::shared_ptr<OptionUint16Array> vendor_oro(new OptionUint16Array(Option::V6, + DOCSIS3_V6_ORO)); + vendor_oro->addValue(DOCSIS3_V6_CONFIG_FILE); // Request option 33 + OptionPtr vendor(new OptionVendor(Option::V6, vendor_id)); + vendor->addOption(vendor_oro); + sol->addOption(vendor); + + // Need to process SOLICIT again after requesting new option. + AllocEngine::ClientContext6 ctx2; + drop = !srv_.earlyGHRLookup(sol, ctx2); + ASSERT_FALSE(drop); + srv_.initContext(sol, ctx2, drop); + ASSERT_FALSE(drop); + adv = srv_.processSolicit(ctx2); + ASSERT_TRUE(adv); + + // Check if there is (or not) a vendor option in the response. + OptionPtr tmp = adv->getOption(D6O_VENDOR_OPTS); + if (vendor_id != VENDOR_ID_CABLE_LABS) { + EXPECT_FALSE(tmp); + return; + } + ASSERT_TRUE(tmp); + + // The response should be an OptionVendor. + boost::shared_ptr<OptionVendor> vendor_resp = + boost::dynamic_pointer_cast<OptionVendor>(tmp); + ASSERT_TRUE(vendor_resp); + + // Option 33 should be present. + OptionPtr docsis33 = vendor_resp->getOption(DOCSIS3_V6_CONFIG_FILE); + ASSERT_TRUE(docsis33); + + // Check that the provided content match the one in configuration. + OptionStringPtr config_file = boost::dynamic_pointer_cast<OptionString>(docsis33); + ASSERT_TRUE(config_file); + EXPECT_EQ("normal_erouter_v6.cm", config_file->getValue()); + } + + /// @brief Test what options a client can use to request vendor options. + void testRequestingOfVendorOptions(vector<int8_t> const& client_options) { + Dhcp6Client client; + + EXPECT_NO_THROW(configure(config_, *client.getServer())); + + bool should_yield_response(false); + for (int8_t const i : client_options) { + OptionPtr vendor_option; + + if (i == D6O_USER_CLASS) { + // An option that should not trigger a response containing + // vendor options. + vendor_option = boost::make_shared<OptionString>(Option::V6, + D6O_USER_CLASS, + "hello"); + } else if (i == D6O_VENDOR_CLASS) { + vendor_option = + boost::make_shared<OptionVendorClass>(Option::V6, + vendor_id_); + should_yield_response = true; + } else if (i == D6O_VENDOR_OPTS) { + vendor_option = boost::make_shared<OptionVendor>(Option::V6, + vendor_id_); + should_yield_response = true; + } else { + continue; + } + client.addExtraOption(vendor_option); + } + + // Let's check whether the server is able to process this packet + // and include the appropriate options. + EXPECT_NO_THROW(client.doSolicit()); + ASSERT_TRUE(client.getContext().response_); + + // Check that there is a response if an option was properly requested. + // Otherwise check that a response has not been provided and stop here. + OptionPtr response( + client.getContext().response_->getOption(D6O_VENDOR_OPTS)); + if (should_yield_response) { + ASSERT_TRUE(response); + } else { + ASSERT_FALSE(response); + return; + } + + // Check that it includes vendor opts with the right vendor ID. + OptionVendorPtr response_vendor_options( + boost::dynamic_pointer_cast<OptionVendor>(response)); + ASSERT_TRUE(response_vendor_options); + EXPECT_EQ(vendor_id_, response_vendor_options->getVendorId()); + + // Check that it contains requested option with the appropriate content. + OptionPtr suboption( + response_vendor_options->getOption(option_)); + ASSERT_TRUE(suboption); + vector<uint8_t> binary_suboption = suboption->toBinary(false); + string text(binary_suboption.begin(), binary_suboption.end()); + EXPECT_EQ("2001:db8::1234:5678", text); + } + +private: + /// @brief Configured option data + string data_ = "2001:db8::1234:5678"; + + /// @brief Configured option code + int32_t option_ = 32; + + /// @brief Configured and requested vendor ID + int32_t vendor_id_ = 32768; + + /// @brief Server configuration + string config_ = R"( + { + "option-data": [ + { + "always-send": true, + "code": )" + to_string(option_) + R"(, + "data": ")" + data_ + R"(", + "name": "tftp-address", + "space": "vendor-)" + to_string(vendor_id_) + R"(" + } + ], + "option-def": [ + { + "code": )" + to_string(option_) + R"(, + "name": "tftp-address", + "space": "vendor-)" + to_string(vendor_id_) + R"(", + "type": "string" + } + ], + "subnet6": [ + { + "interface": "eth0", + "pools": [ + { + "pool": "2001:db8::/64" + } + ], + "subnet": "2001:db8::/64" + } + ] + } + )"; + + std::unique_ptr<IfaceMgrTestConfig> iface_mgr_test_config_; +}; + +TEST_F(VendorOptsTest, dontRequestVendorID) { + testRequestingOfVendorOptions({}); +} + +TEST_F(VendorOptsTest, negativeTestRequestVendorIDWithOption15) { + testRequestingOfVendorOptions({D6O_USER_CLASS}); +} + +TEST_F(VendorOptsTest, requestVendorIDWithOption16) { + testRequestingOfVendorOptions({D6O_VENDOR_CLASS}); +} + +TEST_F(VendorOptsTest, requestVendorIDWithOption17) { + testRequestingOfVendorOptions({D6O_VENDOR_OPTS}); +} + +TEST_F(VendorOptsTest, requestVendorIDWithOptions16And17) { + testRequestingOfVendorOptions({D6O_VENDOR_CLASS, D6O_VENDOR_OPTS}); +} + +TEST_F(VendorOptsTest, requestVendorIDWithOptions17And16) { + testRequestingOfVendorOptions({D6O_VENDOR_OPTS, D6O_VENDOR_CLASS}); +} + +// Checks if server is able to handle a relayed traffic from DOCSIS3.0 modems +TEST_F(VendorOptsTest, docsisVendorOptionsParse) { + + // Let's get a traffic capture from DOCSIS3.0 modem + Pkt6Ptr sol = PktCaptures::captureDocsisRelayedSolicit(); + EXPECT_NO_THROW(sol->unpack()); + + // Check if the packet contain + OptionPtr opt = sol->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(opt); + + boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt); + ASSERT_TRUE(vendor); + + EXPECT_TRUE(vendor->getOption(DOCSIS3_V6_ORO)); + EXPECT_TRUE(vendor->getOption(36)); + EXPECT_TRUE(vendor->getOption(35)); + EXPECT_TRUE(vendor->getOption(DOCSIS3_V6_DEVICE_TYPE)); + EXPECT_TRUE(vendor->getOption(3)); + EXPECT_TRUE(vendor->getOption(4)); + EXPECT_TRUE(vendor->getOption(5)); + EXPECT_TRUE(vendor->getOption(6)); + EXPECT_TRUE(vendor->getOption(7)); + EXPECT_TRUE(vendor->getOption(8)); + EXPECT_TRUE(vendor->getOption(9)); + EXPECT_TRUE(vendor->getOption(DOCSIS3_V6_VENDOR_NAME)); + EXPECT_TRUE(vendor->getOption(15)); + + EXPECT_FALSE(vendor->getOption(20)); + EXPECT_FALSE(vendor->getOption(11)); + EXPECT_FALSE(vendor->getOption(17)); +} + +// Checks if server is able to parse incoming docsis option and extract suboption 1 (docsis ORO) +TEST_F(VendorOptsTest, docsisVendorORO) { + + NakedDhcpv6Srv srv(0); + + // Let's get a traffic capture from DOCSIS3.0 modem + Pkt6Ptr sol = PktCaptures::captureDocsisRelayedSolicit(); + ASSERT_NO_THROW(sol->unpack()); + + // Check if the packet contains vendor options option + OptionPtr opt = sol->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(opt); + + boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt); + ASSERT_TRUE(vendor); + + opt = vendor->getOption(DOCSIS3_V6_ORO); + ASSERT_TRUE(opt); + + OptionUint16ArrayPtr oro = boost::dynamic_pointer_cast<OptionUint16Array>(opt); + EXPECT_TRUE(oro); +} + +// 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); +} + +// 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); +} + +// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491) +// vendor options is parsed correctly and the persistent options are actually assigned. +TEST_F(VendorOptsTest, vendorPersistentOptions) { + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + " \"option-def\": [ {" + " \"name\": \"config-file\"," + " \"code\": 33," + " \"type\": \"string\"," + " \"space\": \"vendor-4491\"" + " } ]," + " \"option-data\": [ {" + " \"name\": \"config-file\"," + " \"space\": \"vendor-4491\"," + " \"data\": \"normal_erouter_v6.cm\"," + " \"always-send\": true" + " }]," + "\"subnet6\": [ { " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"renew-timer\": 1000, " + " \"rebind-timer\": 1000, " + " \"preferred-lifetime\": 3000," + " \"valid-lifetime\": 4000," + " \"interface-id\": \"\"," + " \"interface\": \"eth0\"" + " } ]," + "\"valid-lifetime\": 4000 }"; + + ASSERT_NO_THROW(configure(config)); + + Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + sol->setRemoteAddr(IOAddress("fe80::abcd")); + sol->setIface("eth0"); + sol->setIndex(ETH0_INDEX); + sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + OptionPtr clientid = generateClientId(); + sol->addOption(clientid); + + // Let's add a vendor-option (vendor-id=4491). + OptionPtr vendor(new OptionVendor(Option::V6, 4491)); + sol->addOption(vendor); + + // Pass it to the server and get an advertise + AllocEngine::ClientContext6 ctx; + bool drop = !srv_.earlyGHRLookup(sol, ctx); + ASSERT_FALSE(drop); + srv_.initContext(sol, ctx, drop); + ASSERT_FALSE(drop); + Pkt6Ptr adv = srv_.processSolicit(ctx); + + // check if we get response at all + ASSERT_TRUE(adv); + + // Check if there is vendor option response + OptionPtr tmp = adv->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(tmp); + + // The response should be OptionVendor object + boost::shared_ptr<OptionVendor> vendor_resp = + boost::dynamic_pointer_cast<OptionVendor>(tmp); + ASSERT_TRUE(vendor_resp); + + OptionPtr docsis33 = vendor_resp->getOption(33); + ASSERT_TRUE(docsis33); + + OptionStringPtr config_file = boost::dynamic_pointer_cast<OptionString>(docsis33); + ASSERT_TRUE(config_file); + EXPECT_EQ("normal_erouter_v6.cm", config_file->getValue()); + + // Let's add a vendor-option (vendor-id=4491) with a single sub-option. + // That suboption has code 1 and is a docsis ORO option. + sol->delOption(D6O_VENDOR_OPTS); + boost::shared_ptr<OptionUint16Array> vendor_oro(new OptionUint16Array(Option::V6, + DOCSIS3_V6_ORO)); + vendor_oro->addValue(DOCSIS3_V6_CONFIG_FILE); // Request option 33 + OptionPtr vendor2(new OptionVendor(Option::V6, 4491)); + vendor2->addOption(vendor_oro); + sol->addOption(vendor2); + + // Need to process SOLICIT again after requesting new option. + AllocEngine::ClientContext6 ctx2; + drop = !srv_.earlyGHRLookup(sol, ctx2); + ASSERT_FALSE(drop); + srv_.initContext(sol, ctx2, drop); + ASSERT_FALSE(drop); + adv = srv_.processSolicit(ctx2); + ASSERT_TRUE(adv); + + // Check if there is vendor option response + tmp = adv->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(tmp); + + // The response should be OptionVendor object + vendor_resp = boost::dynamic_pointer_cast<OptionVendor>(tmp); + ASSERT_TRUE(vendor_resp); + + // There should be only one suboption despite config-file is both + // requested and has the always-send flag. + const OptionCollection& opts = vendor_resp->getOptions(); + ASSERT_EQ(1, opts.size()); +} + +// Test checks whether it is possible to use option definitions defined in +// src/lib/dhcp/docsis3_option_defs.h. +TEST_F(VendorOptsTest, vendorOptionsDocsisDefinitions) { + ConstElementPtr x; + string config_prefix = "{ \"interfaces-config\": {" + " \"interfaces\": [ ]" + "}," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + " \"option-data\": [ {" + " \"name\": \"config-file\"," + " \"space\": \"vendor-4491\"," + " \"code\": "; + string config_postfix = "," + " \"data\": \"normal_erouter_v6.cm\"," + " \"csv-format\": true" + " }]," + "\"subnet6\": [ { " + " \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"renew-timer\": 1000, " + " \"rebind-timer\": 1000, " + " \"preferred-lifetime\": 3000," + " \"valid-lifetime\": 4000," + " \"interface-id\": \"\"," + " \"interface\": \"\"" + " } ]," + "\"valid-lifetime\": 4000 }"; + + // There is docsis3 (vendor-id=4491) vendor option 33, which is a + // config-file. Its format is a single string. + string config_valid = config_prefix + "33" + config_postfix; + + // There is no option 99 defined in vendor-id=4491. As there is no + // definition, the config should fail. + string config_bogus = config_prefix + "99" + config_postfix; + + ConstElementPtr json_bogus; + ASSERT_NO_THROW(json_bogus = parseDHCP6(config_bogus)); + ConstElementPtr json_valid; + ASSERT_NO_THROW(json_valid = parseDHCP6(config_valid)); + + NakedDhcpv6Srv srv(0); + + // This should fail (missing option definition) + EXPECT_NO_THROW(x = configureDhcp6Server(srv, json_bogus)); + ASSERT_TRUE(x); + comment_ = isc::config::parseAnswer(rcode_, x); + ASSERT_EQ(1, rcode_); + + // This should work (option definition present) + EXPECT_NO_THROW(x = configureDhcp6Server(srv, json_valid)); + ASSERT_TRUE(x); + comment_ = isc::config::parseAnswer(rcode_, x); + ASSERT_EQ(0, rcode_); +} + +// This test checks that the server will handle a Solicit with the Vendor Class +// having a length of 4 (enterprise-id only). +TEST_F(VendorOptsTest, cableLabsShortVendorClass) { + NakedDhcpv6Srv srv(0); + + // Create a simple Solicit with the 4-byte long vendor class option. + Pkt6Ptr sol = PktCaptures::captureCableLabsShortVendorClass(); + + // Simulate that we have received that traffic + srv.fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + srv.run(); + + // Get Advertise... + ASSERT_FALSE(srv.fake_sent_.empty()); + Pkt6Ptr adv = srv.fake_sent_.front(); + ASSERT_TRUE(adv); + + // This is sent back to client, so port is 546 + EXPECT_EQ(DHCP6_CLIENT_PORT, adv->getRemotePort()); +} + +// Checks that it's possible to have a vendor opts (17) option in the response +// only. Once specific client (Genexis) sends only vendor-class info and +// expects the server to include vendor opts in the response. +TEST_F(VendorOptsTest, vendorOpsInResponseOnly) { + Dhcp6Client client; + + // The config defines custom vendor (17) suboption 2 that conveys + // a TFTP URL. The client doesn't send vendor class (16) or + // vendor opts (17) option, so normal vendor option processing is + // impossible. However, since there's a class defined that matches + // client's packets and that class inserts a vendor opts in the + // response, Kea should be able to figure out the vendor-id and + // then also insert the suboption 2 with the TFTP URL. + string config = + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ ]" + " }," + " \"option-def\": [" + " {" + " \"name\": \"tftp\"," + " \"code\": 2," + " \"space\": \"vendor-25167\"," + " \"type\": \"string\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"cpe_genexis\"," + " \"test\": \"substring(option[15].hex,0,7) == 'HMC1000'\"," + " \"option-data\": [" + " {" + " \"name\": \"vendor-opts\"," + " \"data\": \"25167\"" + " }," + " {" + " \"name\": \"tftp\"," + " \"space\": \"vendor-25167\"," + " \"data\": \"tftp://192.0.2.1/genexis/HMC1000.v1.3.0-R.img\"," + " \"always-send\": true" + " } ]" + " } ]," + "\"subnet6\": [ { " + " \"pools\": [ { \"pool\": \"2001:db8::/64\" } ]," + " \"subnet\": \"2001:db8::/64\", " + " \"interface\": \"eth0\" " + " } ]" + "}"; + + EXPECT_NO_THROW(configure(config, *client.getServer())); + + // A vendor-class identifier (this matches what Genexis hardware sends) + OptionPtr vopt(new OptionString(Option::V6, D6O_USER_CLASS, + "HMC1000.v1.3.0-R,Element-P1090,genexis.eu")); + client.addExtraOption(vopt); + client.requestOption(D6O_VENDOR_OPTS); + + // Let's check whether the server is not able to process this packet + // and include vivso with appropriate sub-options + EXPECT_NO_THROW(client.doSolicit()); + ASSERT_TRUE(client.getContext().response_); + + // Check there's a response. + OptionPtr rsp = client.getContext().response_->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(rsp); + + // Check that it includes vendor opts with vendor-id = 25167 + OptionVendorPtr rsp_vopts = boost::dynamic_pointer_cast<OptionVendor>(rsp); + ASSERT_TRUE(rsp_vopts); + EXPECT_EQ(25167, rsp_vopts->getVendorId()); + + // Now check that it contains suboption 2 with appropriate content. + OptionPtr subopt2 = rsp_vopts->getOption(2); + ASSERT_TRUE(subopt2); + vector<uint8_t> subopt2bin = subopt2->toBinary(false); + string txt(subopt2bin.begin(), subopt2bin.end()); + EXPECT_EQ("tftp://192.0.2.1/genexis/HMC1000.v1.3.0-R.img", txt); +} |