diff options
Diffstat (limited to 'src/bin/dhcp4/tests/vendor_opts_unittest.cc')
-rw-r--r-- | src/bin/dhcp4/tests/vendor_opts_unittest.cc | 1641 |
1 files changed, 1641 insertions, 0 deletions
diff --git a/src/bin/dhcp4/tests/vendor_opts_unittest.cc b/src/bin/dhcp4/tests/vendor_opts_unittest.cc new file mode 100644 index 0000000..08f446d --- /dev/null +++ b/src/bin/dhcp4/tests/vendor_opts_unittest.cc @@ -0,0 +1,1641 @@ +// 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. There are several +// vendor options in DHCPv4: +// +// vivso (125) - vendor independent vendor specific option. This is by far the +// 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. +// 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 +// this is. + +#include <config.h> +#include <asiolink/io_address.h> +#include <cc/command_interpreter.h> +#include <dhcp4/tests/dhcp4_test_utils.h> +#include <dhcp4/dhcp4_srv.h> +#include <dhcp4/json_config_parser.h> +#include <dhcp4/tests/dhcp4_client.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcp/option_int_array.h> +#include <dhcp/option_int.h> +#include <dhcp/option_string.h> +#include <dhcp/option_vendor.h> +#include <dhcp/tests/pkt_captures.h> +#include <dhcp/docsis3_option_defs.h> +#include <dhcp/dhcp4.h> +#include <dhcpsrv/cfgmgr.h> + +#include <gtest/gtest.h> + +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::config; +using namespace isc::dhcp::test; + +/// @brief Class dedicated to testing vendor options in DHCPv4 +/// +/// For the time being it does not provide any additional functionality, but it +/// groups all vendor related tests under a single name. There were too many +/// tests in Dhcpv4SrvTest class anyway. +class VendorOptsTest : public Dhcpv4SrvTest { +public: + /// @brief Called before each test + void SetUp() override { + iface_mgr_test_config_.reset(new IfaceMgrTestConfig(true)); + IfaceMgr::instance().openSockets4(); + } + + /// @brief Called after each test + void TearDown() override { + iface_mgr_test_config_.reset(); + IfaceMgr::instance().closeSockets(); + } + + /// @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 + /// provided when a different vendor ID is given. + void testVendorOptionsORO(int vendor_id) { + // Create a config with a custom option for Cable Labs. + string config = R"( + { + "interfaces-config": { + "interfaces": [ "*" ] + }, + "option-data": [ + { + "code": 2, + "csv-format": true, + "data": "192.0.2.1, 192.0.2.2", + "name": "tftp-servers", + "space": "vendor-4491" + } + ], + "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); + + // Pass it to the server and get an advertise + Pkt4Ptr offer = srv.processDiscover(dis); + + // Check if we get a response at all. + ASSERT_TRUE(offer); + + // We did not include any vendor opts in DISCOVER, so there should be none + // in OFFER. + ASSERT_FALSE(offer->getOption(DHO_VIVSO_SUBOPTIONS)); + + // 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<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)); + vendor->addOption(vendor_oro); + dis->addOption(vendor); + + // Need to process DHCPDISCOVER again after requesting new option. + offer = srv.processDiscover(dis); + ASSERT_TRUE(offer); + + // Check if there is a vendor option in the response, if the Cable Labs + // 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); + 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 2 should be present. + OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS); + ASSERT_TRUE(docsis2); + + // It should be an Option4AddrLst. + Option4AddrLstPtr tftp_srvs = boost::dynamic_pointer_cast<Option4AddrLst>(docsis2); + 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()); + EXPECT_EQ("192.0.2.1", addrs[0].toText()); + EXPECT_EQ("192.0.2.2", addrs[1].toText()); + } + + std::unique_ptr<IfaceMgrTestConfig> iface_mgr_test_config_; +}; + +/// @todo Add more extensive vendor options tests, including multiple +/// vendor options + +// Checks if vendor options are parsed correctly and requested vendor options +// are echoed back. +TEST_F(VendorOptsTest, vendorOptionsDocsis) { + NakedDhcpv4Srv srv(0); + + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + " \"option-data\": [ {" + " \"name\": \"tftp-servers\"," + " \"space\": \"vendor-4491\"," + " \"code\": 2," + " \"data\": \"10.253.175.16\"," + " \"csv-format\": true" + " }]," + "\"subnet4\": [ { " + " \"pools\": [ { \"pool\": \"10.254.226.0/25\" } ]," + " \"subnet\": \"10.254.226.0/24\", " + " \"interface\": \"eth0\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Let's create a relayed DISCOVER. This particular relayed DISCOVER has + // added option 82 (relay agent info) with 3 suboptions. The server + // is supposed to echo it back in its response. + Pkt4Ptr dis; + ASSERT_NO_THROW(dis = PktCaptures::captureRelayedDiscover()); + + // Simulate that we have received that traffic + srv.fakeReceive(dis); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive4(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered buffer4_receive callback. + srv.run(); + + // Check that the server did send a response + ASSERT_EQ(1, srv.fake_sent_.size()); + + // Make sure that we received a response + Pkt4Ptr offer = srv.fake_sent_.front(); + ASSERT_TRUE(offer); + + // Get Relay Agent Info from query... + OptionPtr vendor_opt_response = offer->getOption(DHO_VIVSO_SUBOPTIONS); + 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); + ASSERT_TRUE(vendor_opt); + + // Get Relay Agent Info from response... + OptionPtr tftp_servers_generic = vendor_opt->getOption(DOCSIS3_V4_TFTP_SERVERS); + ASSERT_TRUE(tftp_servers_generic); + + Option4AddrLstPtr tftp_servers = + boost::dynamic_pointer_cast<Option4AddrLst>(tftp_servers_generic); + + ASSERT_TRUE(tftp_servers); + + Option4AddrLst::AddressContainer addrs = tftp_servers->getAddresses(); + ASSERT_EQ(1, addrs.size()); + EXPECT_EQ("10.253.175.16", addrs[0].toText()); +} + +// 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 + Pkt4Ptr dis = PktCaptures::captureRelayedDiscover(); + ASSERT_NO_THROW(dis->unpack()); + + // Check if the packet contain + OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS); + ASSERT_TRUE(opt); + + boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt); + ASSERT_TRUE(vendor); + + // This particular capture that we have included options 1 and 5 + EXPECT_TRUE(vendor->getOption(1)); + EXPECT_TRUE(vendor->getOption(5)); + + // It did not include options any other options + EXPECT_FALSE(vendor->getOption(2)); + EXPECT_FALSE(vendor->getOption(3)); + 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) { + + // Let's get a traffic capture from DOCSIS3.0 modem + Pkt4Ptr dis = PktCaptures::captureRelayedDiscover(); + EXPECT_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); + ASSERT_TRUE(vendor); + + opt = vendor->getOption(DOCSIS3_V4_ORO); + ASSERT_TRUE(opt); + + OptionUint8ArrayPtr oro = boost::dynamic_pointer_cast<OptionUint8Array>(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 persistent options are actually assigned. +TEST_F(VendorOptsTest, vendorPersistentOptions) { + NakedDhcpv4Srv srv(0); + + 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\" " + " } ]" + "}"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + + EXPECT_NO_THROW(x = configureDhcp4Server(srv, json)); + ASSERT_TRUE(x); + comment_ = parseAnswer(rcode_, x); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + 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); + + 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); + + // Let's add a vendor-option (vendor-id=4491). + OptionPtr vendor(new OptionVendor(Option::V4, 4491)); + dis->addOption(vendor); + + // Pass it to the server and get an advertise + Pkt4Ptr offer = srv.processDiscover(dis); + + // check if we get response at all + ASSERT_TRUE(offer); + + // Check if there is a vendor option response + OptionPtr tmp = offer->getOption(DHO_VIVSO_SUBOPTIONS); + 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 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()); +} + +// 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\": [ ]" + "}," + " \"option-data\": [ {" + " \"name\": \"tftp-servers\"," + " \"space\": \"vendor-4491\"," + " \"code\": "; + string config_postfix = "," + " \"data\": \"192.0.2.1\"," + " \"csv-format\": true" + " }]," + "\"subnet4\": [ { " + " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.50\" } ]," + " \"subnet\": \"192.0.2.0/24\", " + " \"interface\": \"\"" + " } ]" + "}"; + + // There is docsis3 (vendor-id=4491) vendor option 2, which is a + // tftp-server. Its format is list of IPv4 addresses. + string config_valid = config_prefix + "2" + 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 = parseDHCP4(config_bogus)); + ConstElementPtr json_valid; + ASSERT_NO_THROW(json_valid = parseDHCP4(config_valid)); + + NakedDhcpv4Srv srv(0); + + // This should fail (missing option definition) + EXPECT_NO_THROW(x = configureDhcp4Server(srv, json_bogus)); + ASSERT_TRUE(x); + comment_ = parseAnswer(rcode_, x); + ASSERT_EQ(1, rcode_); + + // This should work (option definition present) + EXPECT_NO_THROW(x = configureDhcp4Server(srv, json_valid)); + ASSERT_TRUE(x); + comment_ = parseAnswer(rcode_, x); + ASSERT_EQ(0, rcode_); +} + +/// Checks if DOCSIS client packets are classified properly +/// +/// The test has been updated to work with the updated generic +/// vendor options handling code. +TEST_F(VendorOptsTest, docsisClientClassification) { + + NakedDhcpv4Srv srv(0); + + // Let's create a relayed DISCOVER. This particular relayed DISCOVER has + // vendor-class set to docsis3.0 + Pkt4Ptr dis1; + ASSERT_NO_THROW(dis1 = PktCaptures::captureRelayedDiscover()); + ASSERT_NO_THROW(dis1->unpack()); + + srv.classifyPacket(dis1); + + EXPECT_TRUE(dis1->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0:")); + EXPECT_FALSE(dis1->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0")); + + // Let's create a relayed DISCOVER. This particular relayed DISCOVER has + // vendor-class set to eRouter1.0 + Pkt4Ptr dis2; + ASSERT_NO_THROW(dis2 = PktCaptures::captureRelayedDiscover2()); + ASSERT_NO_THROW(dis2->unpack()); + + srv.classifyPacket(dis2); + + EXPECT_TRUE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0")); + EXPECT_FALSE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0:")); +} + +// Checks that it's possible to have a vivso (125) option in the response +// only. Once specific client (Genexis) sends only vendor-class info and +// expects the server to include vivso in the response. +TEST_F(VendorOptsTest, vivsoInResponseOnly) { + Dhcp4Client client; + + // The config defines custom vendor 125 suboption 2 that conveys a TFTP URL. + // The client doesn't send vendor 125 option, so normal vendor option + // processing is impossible. However, since there's a class defined that + // matches client's packets and that class inserts vivso in the response, + // Kea should be able to figure out the vendor-id and then also insert + // 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[60].hex,0,7) == 'HMC1000'\"," + " \"option-data\": [" + " {" + " \"name\": \"vivso-suboptions\"," + " \"data\": \"25167\"" + " }," + " {" + " \"name\": \"tftp\"," + " \"space\": \"vendor-25167\"," + " \"data\": \"tftp://192.0.2.1/genexis/HMC1000.v1.3.0-R.img\"," + " \"always-send\": true" + " } ]" + " } ]," + "\"subnet4\": [ { " + " \"pools\": [ { \"pool\": \"192.0.2.0/25\" } ]," + " \"subnet\": \"192.0.2.0/24\", " + " \"interface\": \"eth0\" " + " } ]" + "}"; + + EXPECT_NO_THROW(configure(config, *client.getServer())); + + // Add a vendor-class identifier (this matches what Genexis hardware sends) + OptionPtr vopt(new OptionString(Option::V4, DHO_VENDOR_CLASS_IDENTIFIER, + "HMC1000.v1.3.0-R,Element-P1090,genexis.eu")); + client.addExtraOption(vopt); + client.requestOptions(DHO_VIVSO_SUBOPTIONS); + + // Let's check whether the server is not able to process this packet + // and include vivso with appropriate sub-options + EXPECT_NO_THROW(client.doDiscover()); + ASSERT_TRUE(client.getContext().response_); + + // Check there's a response. + OptionPtr rsp = client.getContext().response_->getOption(DHO_VIVSO_SUBOPTIONS); + ASSERT_TRUE(rsp); + + // 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()); + + // Now check that it contains suboption 2 with appropriate content. + OptionPtr subopt2 = rsp_vivso->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); +} + +// Verifies last resort option 43 is backward compatible +TEST_F(VendorOptsTest, option43LastResort) { + NakedDhcpv4Srv srv(0); + + // If there is no definition for option 43 a last resort + // one is applied. This definition was used by Kea <= 1.2 + // so should be backward compatible. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"vendor-encapsulated-options-space\", " + " \"type\": \"uint32\" } ]," + "\"option-data\": [ " + "{ \"name\": \"foo\", " + " \"space\": \"vendor-encapsulated-options-space\", " + " \"data\": \"12345678\" }, " + "{ \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + "{ \"name\": \"vendor-encapsulated-options\" } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + const OptionCollection& opts = opt->getOptions(); + ASSERT_EQ(1, opts.size()); + OptionPtr sopt = opts.begin()->second; + ASSERT_TRUE(sopt); + EXPECT_EQ(1, sopt->getType()); +} + +// Checks effect of raw not compatible option 43 (no failure) +TEST_F(VendorOptsTest, option43BadRaw) { + NakedDhcpv4Srv srv(0); + + // The vendor-encapsulated-options has an incompatible data + // so won't have the expected content but processing of truncated + // (suboption length > available length) suboptions does not raise + // an exception. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"option-data\": [ " + "{ \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + "{ \"name\": \"vendor-encapsulated-options\", " + " \"csv-format\": false, " + " \"data\": \"0102\" } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x02); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + srv.deferredUnpack(query); + + // Check if the option was (uncorrectly) re-unpacked + vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt); + EXPECT_TRUE(custom); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + // But truncated. + EXPECT_EQ(0, opt->len() - opt->getHeaderLen()); +} + +// Checks effect of raw not compatible option 43 (failure) +TEST_F(VendorOptsTest, option43FailRaw) { + NakedDhcpv4Srv srv(0); + + // The vendor-encapsulated-options has an incompatible data + // so won't have the expected content. Here the processing + // of suboptions tries to unpack the uitn32 foo suboption and + // raises an exception which is caught so the option stays unpacked. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"vendor-encapsulated-options-space\", " + " \"type\": \"uint32\" } ]," + "\"option-data\": [ " + "{ \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + "{ \"name\": \"vendor-encapsulated-options\", " + " \"csv-format\": false, " + " \"data\": \"0102\" } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + // which will raise an exception + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x01); + buf.push_back(0x01); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + EXPECT_NO_THROW(srv.deferredUnpack(query)); + ASSERT_TRUE(query->getOption(vopt->getType())); + EXPECT_EQ(vopt, query->getOption(vopt->getType())); +} + +// Verifies raw option 43 can be handled (global) +TEST_F(VendorOptsTest, option43RawGlobal) { + NakedDhcpv4Srv srv(0); + + // The vendor-encapsulated-options is redefined as raw binary + // in a global definition. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"option-def\": [ " + "{ \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"binary\" } ]," + "\"option-data\": [ " + "{ \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + "{ \"name\": \"vendor-encapsulated-options\", " + " \"csv-format\": false, " + " \"data\": \"0102\" } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + OptionBuffer buf; + buf.push_back(0x02); + buf.push_back(0x03); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Check if the option was (correctly) re-unpacked + vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt); + EXPECT_FALSE(custom); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + // Verifies the content + ASSERT_EQ(2, opt->len() - opt->getHeaderLen()); + EXPECT_EQ(0x01, opt->getData()[0]); + EXPECT_EQ(0x02, opt->getData()[1]); +} + +// Verifies raw option 43 can be handled (catch-all class) +TEST_F(VendorOptsTest, option43RawClass) { + NakedDhcpv4Srv srv(0); + + // The vendor-encapsulated-options is redefined as raw binary + // in a class definition. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"client-classes\": [ " + "{ \"name\": \"vendor\", " + " \"test\": \"option[vendor-encapsulated-options].exists\", " + " \"option-def\": [ " + " { \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"binary\" } ]," + " \"option-data\": [ " + " { \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + " { \"name\": \"vendor-encapsulated-options\", " + " \"csv-format\": false, " + " \"data\": \"0102\" } ] } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + OptionBuffer buf; + buf.push_back(0x02); + buf.push_back(0x03); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Check if the option was (correctly) re-unpacked + vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt); + EXPECT_FALSE(custom); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + // Verifies the content + ASSERT_EQ(2, opt->len() - opt->getHeaderLen()); + EXPECT_EQ(0x01, opt->getData()[0]); + EXPECT_EQ(0x02, opt->getData()[1]); +} + +// Verifies option 43 deferred processing (one class) +TEST_F(VendorOptsTest, option43Class) { + NakedDhcpv4Srv srv(0); + + // A client class defines vendor-encapsulated-options (code 43) + // and data for it and its sub-option. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"alpha\", " + " \"type\": \"uint32\" } ]," + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"client-classes\": [ " + "{ \"name\": \"alpha\", " + " \"test\": \"option[vendor-class-identifier].text == 'alpha'\", " + " \"option-def\": [ " + " { \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"empty\", " + " \"encapsulate\": \"alpha\" } ]," + " \"option-data\": [ " + " { \"name\": \"vendor-class-identifier\", " + " \"data\": \"alpha\" }, " + " { \"name\": \"vendor-encapsulated-options\" }, " + " { \"name\": \"foo\", " + " \"space\": \"alpha\", " + " \"data\": \"12345678\" } ] } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x04); + buf.push_back(0x87); + buf.push_back(0x65); + buf.push_back(0x43); + buf.push_back(0x21); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a vendor-class-identifier (code 60) + OptionStringPtr iopt(new OptionString(Option::V4, + DHO_VENDOR_CLASS_IDENTIFIER, + "alpha")); + query->addOption(iopt); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Check if the option was (correctly) re-unpacked + vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt); + EXPECT_TRUE(custom); + EXPECT_EQ(1, vopt->getOptions().size()); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + // Verifies the content + const OptionCollection& opts = opt->getOptions(); + ASSERT_EQ(1, opts.size()); + OptionPtr sopt = opts.begin()->second; + ASSERT_TRUE(sopt); + EXPECT_EQ(1, sopt->getType()); + OptionUint32Ptr sopt32 = boost::dynamic_pointer_cast<OptionUint32>(sopt); + ASSERT_TRUE(sopt32); + EXPECT_EQ(12345678, sopt32->getValue()); +} + +// Verifies option 43 priority +TEST_F(VendorOptsTest, option43ClassPriority) { + NakedDhcpv4Srv srv(0); + + // Both global and client-class scopes get vendor-encapsulated-options + // (code 43) definition and data. The client-class has precedence. + // Note it does not work without the vendor-encapsulated-options + // option-data in the client-class. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"alpha\", " + " \"type\": \"uint32\" }," + "{ \"code\": 1, " + " \"name\": \"bar\", " + " \"space\": \"beta\", " + " \"type\": \"uint8\" }, " + "{ \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"empty\", " + " \"encapsulate\": \"beta\" } ]," + "\"option-data\": [ " + "{ \"name\": \"vendor-encapsulated-options\" }, " + "{ \"name\": \"vendor-class-identifier\", " + " \"data\": \"beta\" }, " + "{ \"name\": \"bar\", " + " \"space\": \"beta\", " + " \"data\": \"33\" } ]," + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"client-classes\": [ " + "{ \"name\": \"alpha\", " + " \"test\": \"option[vendor-class-identifier].text == 'alpha'\", " + " \"option-def\": [ " + " { \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"empty\", " + " \"encapsulate\": \"alpha\" } ]," + " \"option-data\": [ " + "{ \"name\": \"vendor-encapsulated-options\" }, " + " { \"name\": \"vendor-class-identifier\", " + " \"data\": \"alpha\" }, " + " { \"name\": \"foo\", " + " \"space\": \"alpha\", " + " \"data\": \"12345678\" } ] } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x04); + buf.push_back(0x87); + buf.push_back(0x65); + buf.push_back(0x43); + buf.push_back(0x21); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a vendor-class-identifier (code 60) + OptionStringPtr iopt(new OptionString(Option::V4, + DHO_VENDOR_CLASS_IDENTIFIER, + "alpha")); + query->addOption(iopt); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Check if the option was (correctly) re-unpacked + vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt); + EXPECT_TRUE(custom); + EXPECT_EQ(1, vopt->getOptions().size()); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + OptionStringPtr id = boost::dynamic_pointer_cast<OptionString>(opt); + ASSERT_TRUE(id); + EXPECT_EQ("alpha", id->getValue()); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + // Verifies the content + const OptionCollection& opts = opt->getOptions(); + ASSERT_EQ(1, opts.size()); + OptionPtr sopt = opts.begin()->second; + ASSERT_TRUE(sopt); + EXPECT_EQ(1, sopt->getType()); + EXPECT_EQ(2 + 4, sopt->len()); + OptionUint32Ptr sopt32 = boost::dynamic_pointer_cast<OptionUint32>(sopt); + ASSERT_TRUE(sopt32); + EXPECT_EQ(12345678, sopt32->getValue()); +} + +// Verifies option 43 deferred processing (two classes) +TEST_F(VendorOptsTest, option43Classes) { + NakedDhcpv4Srv srv(0); + + // Two client-class scopes get vendor-encapsulated-options + // (code 43) definition and data. The first matching client-class + // (from a set?) applies. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"alpha\", " + " \"type\": \"uint32\" }," + "{ \"code\": 1, " + " \"name\": \"bar\", " + " \"space\": \"beta\", " + " \"type\": \"uint8\" } ]," + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], " + " \"subnet\": \"192.0.2.0/24\" } ]," + "\"client-classes\": [ " + "{ \"name\": \"alpha\", " + " \"test\": \"option[vendor-class-identifier].text == 'alpha'\", " + " \"option-def\": [ " + " { \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"empty\", " + " \"encapsulate\": \"alpha\" } ]," + " \"option-data\": [ " + "{ \"name\": \"vendor-encapsulated-options\" }, " + " { \"name\": \"vendor-class-identifier\", " + " \"data\": \"alpha\" }, " + " { \"name\": \"foo\", " + " \"space\": \"alpha\", " + " \"data\": \"12345678\" } ] }," + "{ \"name\": \"beta\", " + " \"test\": \"option[vendor-class-identifier].text == 'beta'\", " + " \"option-def\": [ " + " { \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"empty\", " + " \"encapsulate\": \"beta\" } ]," + " \"option-data\": [ " + "{ \"name\": \"vendor-encapsulated-options\" }, " + " { \"name\": \"vendor-class-identifier\", " + " \"data\": \"beta\" }, " + " { \"name\": \"bar\", " + " \"space\": \"beta\", " + " \"data\": \"33\" } ] } ] }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a vendor-encapsulated-options (code 43) + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x04); + buf.push_back(0x87); + buf.push_back(0x65); + buf.push_back(0x43); + buf.push_back(0x21); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + query->addOption(vopt); + query->getDeferredOptions().push_back(DHO_VENDOR_ENCAPSULATED_OPTIONS); + + // Create and add a vendor-class-identifier (code 60) + OptionStringPtr iopt(new OptionString(Option::V4, + DHO_VENDOR_CLASS_IDENTIFIER, + "alpha")); + query->addOption(iopt); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Check if the option was (correctly) re-unpacked + vopt = query->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + OptionCustomPtr custom = boost::dynamic_pointer_cast<OptionCustom>(vopt); + EXPECT_TRUE(custom); + EXPECT_EQ(1, vopt->getOptions().size()); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-class-identifier (code 60) + OptionPtr opt = offer->getOption(DHO_VENDOR_CLASS_IDENTIFIER); + EXPECT_TRUE(opt); + OptionStringPtr id = boost::dynamic_pointer_cast<OptionString>(opt); + ASSERT_TRUE(id); + EXPECT_EQ("alpha", id->getValue()); + + // And a vendor-encapsulated-options (code 43) + opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + // Verifies the content + const OptionCollection& opts = opt->getOptions(); + ASSERT_EQ(1, opts.size()); + OptionPtr sopt = opts.begin()->second; + ASSERT_TRUE(sopt); + EXPECT_EQ(1, sopt->getType()); + EXPECT_EQ(2 + 4, sopt->len()); + OptionUint32Ptr sopt32 = boost::dynamic_pointer_cast<OptionUint32>(sopt); + ASSERT_TRUE(sopt32); + EXPECT_EQ(12345678, sopt32->getValue()); +} + +// Checks effect of raw not compatible option 43 sent by a client (failure) +TEST_F(VendorOptsTest, clientOption43FailRaw) { + Dhcp4Client client; + + // The vendor-encapsulated-options has an incompatible data + // so won't have the expected content. Here the processing + // of suboptions tries to unpack the uint32 foo suboption and + // raises an exception which is caught. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ], " + " \"subnet\": \"10.0.0.0/24\" } ]," + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"vendor-encapsulated-options-space\", " + " \"type\": \"uint32\" } ] }"; + + EXPECT_NO_THROW(configure(config, *client.getServer())); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + // which will raise an exception + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x01); + buf.push_back(0x01); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + client.addExtraOption(vopt); + + // Let's check whether the server is not able to process this packet + // and raises an exception which is caught so the response is not empty. + EXPECT_NO_THROW(client.doDiscover()); + EXPECT_TRUE(client.getContext().response_); +} + +// Verifies raw option 43 sent by a client can be handled (global) +TEST_F(VendorOptsTest, clientOption43RawGlobal) { + Dhcp4Client client; + + // The vendor-encapsulated-options is redefined as raw binary + // in a global definition. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ], " + " \"subnet\": \"10.0.0.0/24\" } ]," + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"vendor-encapsulated-options-space\", " + " \"type\": \"uint32\" }," + "{ \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"binary\" } ]," + "\"option-data\": [ " + "{ \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + "{ \"name\": \"vendor-encapsulated-options\", " + " \"csv-format\": false, " + " \"data\": \"0102\" } ] }"; + + EXPECT_NO_THROW(configure(config, *client.getServer())); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x01); + buf.push_back(0x01); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + client.addExtraOption(vopt); + + // Let's check whether the server is able to process this packet without + // throwing any exceptions so the response is not empty. + EXPECT_NO_THROW(client.doDiscover()); + EXPECT_TRUE(client.getContext().response_); +} + +// Verifies raw option 43 sent by a client can be handled (catch-all class) +TEST_F(VendorOptsTest, clientOption43RawClass) { + Dhcp4Client client; + + // The vendor-encapsulated-options is redefined as raw binary + // in a class definition. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ] }, " + "\"subnet4\": [ " + "{ \"pools\": [ { \"pool\": \"10.0.0.10 - 10.0.0.100\" } ], " + " \"subnet\": \"10.0.0.0/24\" } ]," + "\"option-def\": [ " + "{ \"code\": 1, " + " \"name\": \"foo\", " + " \"space\": \"vendor-encapsulated-options-space\", " + " \"type\": \"uint32\" } ]," + "\"client-classes\": [ " + "{ \"name\": \"vendor\", " + " \"test\": \"option[vendor-encapsulated-options].exists\", " + " \"option-def\": [ " + " { \"code\": 43, " + " \"name\": \"vendor-encapsulated-options\", " + " \"type\": \"binary\" } ]," + " \"option-data\": [ " + " { \"name\": \"vendor-class-identifier\", " + " \"data\": \"bar\" }, " + " { \"name\": \"vendor-encapsulated-options\", " + " \"csv-format\": false, " + " \"data\": \"0102\" } ] } ] }"; + + EXPECT_NO_THROW(configure(config, *client.getServer())); + + // Create and add a vendor-encapsulated-options (code 43) + // with not compatible (not parsable as suboptions) content + OptionBuffer buf; + buf.push_back(0x01); + buf.push_back(0x01); + buf.push_back(0x01); + OptionPtr vopt(new Option(Option::V4, DHO_VENDOR_ENCAPSULATED_OPTIONS, buf)); + client.addExtraOption(vopt); + + // Let's check whether the server is able to process this packet without + // throwing any exceptions so the response is not empty. + EXPECT_NO_THROW(client.doDiscover()); + EXPECT_TRUE(client.getContext().response_); +} + +// Verifies that a client query with a truncated length in +// vendor option (125) will still be processed by the server. +TEST_F(Dhcpv4SrvTest, truncatedVIVSOOption) { + NakedDhcpv4Srv srv(0); + + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"subnet4\": [ { " + " \"pools\": [ { \"pool\": \"10.206.80.0/25\" } ]," + " \"subnet\": \"10.206.80.0/24\", " + " \"rebind-timer\": 2000, " + " \"renew-timer\": 1000, " + " \"valid-lifetime\": 4000" + " } ]" + "}"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_) << isc::data::prettyPrint(status); + + CfgMgr::instance().commit(); + + // Create a DISCOVER with a VIVSO option whose length is + // too short. + Pkt4Ptr dis; + ASSERT_NO_THROW(dis = PktCaptures::discoverWithTruncatedVIVSO()); + + // Simulate that we have received that traffic + srv.fakeReceive(dis); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive4(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered buffer4_receive callback. + srv.run(); + + // Check that the server did send a response + ASSERT_EQ(1, srv.fake_sent_.size()); + + // Make sure that we received an response and it was a DHCPOFFER. + Pkt4Ptr offer = srv.fake_sent_.front(); + ASSERT_TRUE(offer); +} + +/// Checks that it's possible to define and use a suboption 0. +TEST_F(VendorOptsTest, vendorOpsSubOption0) { + NakedDhcpv4Srv srv(0); + + // Zero Touch provisioning + string config = + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"option-def\": [" + " {" + " \"name\": \"vendor-encapsulated-options\"," + " \"code\": 43," + " \"type\": \"empty\"," + " \"encapsulate\": \"ZTP\"" + " }," + " {" + " \"name\": \"config-file-name\"," + " \"code\": 1," + " \"space\": \"ZTP\"," + " \"type\": \"string\"" + " }," + " {" + " \"name\": \"image-file-name\"," + " \"code\": 0," + " \"space\": \"ZTP\"," + " \"type\": \"string\"" + " }," + " {" + " \"name\": \"image-file-type\"," + " \"code\": 2," + " \"space\": \"ZTP\"," + " \"type\": \"string\"" + " }," + " {" + " \"name\": \"transfer-mode\"," + " \"code\": 3," + " \"space\": \"ZTP\"," + " \"type\": \"string\"" + " }," + " {" + " \"name\": \"all-image-file-name\"," + " \"code\": 4," + " \"space\": \"ZTP\"," + " \"type\": \"string\"" + " }," + " {" + " \"name\": \"http-port\"," + " \"code\": 5," + " \"space\": \"ZTP\"," + " \"type\": \"string\"" + " }" + " ]," + " \"option-data\": [" + " {" + " \"name\": \"vendor-encapsulated-options\"" + " }," + " {" + " \"name\": \"image-file-name\"," + " \"data\": \"/dist/images/jinstall-ex.tgz\"," + " \"space\": \"ZTP\"" + " }" + " ]," + "\"subnet4\": [ { " + " \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ]," + " \"subnet\": \"192.0.2.0/24\"" + " } ]" + "}"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config, true)); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + CfgMgr::instance().commit(); + + // Create a packet with enough to select the subnet and go through + // the DISCOVER processing + Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234)); + query->setRemoteAddr(IOAddress("192.0.2.1")); + OptionPtr clientid = generateClientId(); + query->addOption(clientid); + query->setIface("eth1"); + query->setIndex(ETH1_INDEX); + + // Create and add a PRL option to the query + OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4, + DHO_DHCP_PARAMETER_REQUEST_LIST)); + ASSERT_TRUE(prl); + prl->addValue(DHO_VENDOR_ENCAPSULATED_OPTIONS); + prl->addValue(DHO_VENDOR_CLASS_IDENTIFIER); + query->addOption(prl); + + srv.classifyPacket(query); + ASSERT_NO_THROW(srv.deferredUnpack(query)); + + // Pass it to the server and get a DHCPOFFER. + Pkt4Ptr offer = srv.processDiscover(query); + + // Check if we get response at all + checkResponse(offer, DHCPOFFER, 1234); + + // Processing should add a vendor-encapsulated-options (code 43) + OptionPtr opt = offer->getOption(DHO_VENDOR_ENCAPSULATED_OPTIONS); + ASSERT_TRUE(opt); + const OptionCollection& opts = opt->getOptions(); + ASSERT_EQ(1, opts.size()); + OptionPtr sopt = opts.begin()->second; + ASSERT_TRUE(sopt); + EXPECT_EQ(0, sopt->getType()); + + // Check suboption 0 content. + OptionStringPtr sopt0 = + boost::dynamic_pointer_cast<OptionString>(sopt); + ASSERT_TRUE(sopt0); + EXPECT_EQ("/dist/images/jinstall-ex.tgz", sopt0->getValue()); +} |