From: Marcin Siodelski Date: Tue, 22 Oct 2013 10:27:07 +0000 (+0200) Subject: [3194] Implemented basic vendor options support. X-Git-Tag: bind10-1.2.0beta1-release~102^2~45^2~11 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=0764a74e461d71c8d3dcb25dc2399a4d18d553f8;p=thirdparty%2Fkea.git [3194] Implemented basic vendor options support. --- diff --git a/src/bin/dhcp4/config_parser.cc b/src/bin/dhcp4/config_parser.cc index 98393c1560..ddf5ed454a 100644 --- a/src/bin/dhcp4/config_parser.cc +++ b/src/bin/dhcp4/config_parser.cc @@ -96,6 +96,13 @@ protected: << " for DHCPv6 server"); } + // Check if this is a vendor-option. If it is, get vendor-specific + // definition. + uint32_t vendor_id = SubnetConfigParser::optionSpaceToVendorId(option_space); + if (vendor_id) { + def = LibDHCP::getVendorOptionDef(Option::V4, vendor_id, option_code); + } + return (def); } }; diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index f2903baa67..aeb47298d8 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/src/bin/dhcp4/dhcp4_srv.cc @@ -20,7 +20,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -36,6 +38,7 @@ #include #include +#include #include #include @@ -643,6 +646,62 @@ Dhcpv4Srv::appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) { } } +void +Dhcpv4Srv::appendRequestedVendorOptions(const Pkt4Ptr& question, Pkt4Ptr& answer) { + // Get the configured subnet suitable for the incoming packet. + Subnet4Ptr subnet = selectSubnet(question); + // 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) { + return; + } + + // Try to get the vendor option + boost::shared_ptr vendor_req = + boost::dynamic_pointer_cast(question->getOption(DHO_VIVSO_SUBOPTIONS)); + if (!vendor_req) { + return; + } + + uint32_t vendor_id = vendor_req->getVendorId(); + + // Let's try to get ORO within that vendor-option + /// @todo This is very specific to vendor-id=4491 (Cable Labs). Other vendors + /// may have different policies. + OptionPtr oro = vendor_req->getOption(DOCSIS3_V4_ORO); + + /// @todo: see OPT_UINT8_TYPE definition in OptionDefinition::optionFactory(). + /// I think it should be OptionUint8Array, not OptionGeneric + + // Option ORO not found. Don't do anything then. + if (!oro) { + return; + } + + boost::shared_ptr vendor_rsp(new OptionVendor(Option::V4, vendor_id)); + + // Get the list of options that client requested. + bool added = false; + const OptionBuffer& requested_opts = oro->getData(); + + for (OptionBuffer::const_iterator code = requested_opts.begin(); + code != requested_opts.end(); ++code) { + Subnet::OptionDescriptor desc = subnet->getVendorOptionDescriptor(vendor_id, *code); + if (desc.option) { + vendor_rsp->addOption(desc.option); + added = true; + } + } + + if (added) { + answer->addOption(vendor_rsp); + } +} + + void Dhcpv4Srv::appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) { // Identify options that we always want to send to the @@ -858,6 +917,7 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) { copyDefaultFields(discover, offer); appendDefaultOptions(offer, DHCPOFFER); appendRequestedOptions(discover, offer); + appendRequestedVendorOptions(discover, offer); assignLease(discover, offer); @@ -881,6 +941,7 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) { copyDefaultFields(request, ack); appendDefaultOptions(ack, DHCPACK); appendRequestedOptions(request, ack); + appendRequestedVendorOptions(request, ack); // Note that we treat REQUEST message uniformly, regardless if this is a // first request (requesting for new address), renewing existing address @@ -1226,6 +1287,25 @@ Dhcpv4Srv::unpackOptions(const OptionBuffer& buf, << "-byte long buffer."); } + /// @todo: Not sure if this is needed. Perhaps it would be better to extend + /// DHO_VIVSO_SUBOPTIONS definitions in std_option_defs.h to cover + /// OptionVendor class? + if (opt_type == DHO_VIVSO_SUBOPTIONS) { + if (offset + 4 > buf.size()) { + // Truncated vendor-option. There is expected at least 4 bytes + // long enterprise-id field + return (offset); + } + + // Parse this as vendor option + OptionPtr vendor_opt(new OptionVendor(Option::V4, buf.begin() + offset, + buf.begin() + offset + opt_len)); + options.insert(std::make_pair(opt_type, vendor_opt)); + + offset += opt_len; + continue; + } + // Get all definitions with the particular option code. Note that option code // is non-unique within this container however at this point we expect // to get one option definition with the particular code. If more are diff --git a/src/bin/dhcp4/dhcp4_srv.h b/src/bin/dhcp4/dhcp4_srv.h index 5c89c85770..66fe3a6315 100644 --- a/src/bin/dhcp4/dhcp4_srv.h +++ b/src/bin/dhcp4/dhcp4_srv.h @@ -227,6 +227,18 @@ protected: /// @param msg outgoing message (options will be added here) void appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg); + /// @brief Appends requested vendor options as requested by client. + /// + /// This method is similar to \ref appendRequestedOptions(), but uses + /// vendor options. The major difference is that vendor-options use + /// its own option spaces (there may be more than one distinct set of vendor + /// options, each with unique vendor-id). Vendor options are requested + /// using separate options within their respective vendor-option spaces. + /// + /// @param question DISCOVER or REQUEST message from a client. + /// @param msg outgoing message (options will be added here) + void appendRequestedVendorOptions(const Pkt4Ptr& question, Pkt4Ptr& answer); + /// @brief Assigns a lease and appends corresponding options /// /// This method chooses the most appropriate lease for reqesting diff --git a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc index 3f2da84f9b..4d2ba565d4 100644 --- a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc @@ -25,8 +25,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -1138,11 +1140,7 @@ TEST_F(Dhcpv4SrvTest, ServerID) { EXPECT_EQ(srvid_text, text); } -// Checks if callouts installed on pkt4_receive are indeed called and the -// all necessary parameters are passed. -// -// Note that the test name does not follow test naming convention, -// but the proper hook name is "buffer4_receive". +// Checks if received relay agent info option is echoed back to the client TEST_F(Dhcpv4SrvTest, relayAgentInfoEcho) { NakedDhcpv4Srv srv(0); @@ -1180,6 +1178,87 @@ TEST_F(Dhcpv4SrvTest, relayAgentInfoEcho) { EXPECT_TRUE(rai_response->equal(rai_query)); } +// Checks if vendor options are parsed correctly and requested vendor options +// are echoed back. +TEST_F(Dhcpv4SrvTest, vendorOptionsDocsis) { + + NakedDhcpv4Srv srv(0); + + string config = "{ \"interfaces\": [ \"*\" ]," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + " \"option-data\": [ {" + " \"name\": \"tftp-servers\"," + " \"space\": \"vendor-4491\"," + " \"code\": 2," + " \"data\": \"10.253.175.16\"," + " \"csv-format\": True" + " }]," + "\"subnet4\": [ { " + " \"pool\": [ \"10.254.226.0/25\" ]," + " \"subnet\": \"10.254.226.0/24\", " + " \"interface\": \"" + valid_iface_ + "\" " + " }, {" + " \"pool\": [ \"192.0.3.0/25\" ]," + " \"subnet\": \"192.0.3.0/24\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ElementPtr json = Element::fromJSON(config); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + ASSERT_TRUE(status); + comment_ = config::parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + // 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 = 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 reposonse + 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 vendor_opt = + boost::dynamic_pointer_cast(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(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()); +} + + /// @todo Implement tests for subnetSelect See tests in dhcp6_srv_unittest.cc: /// selectSubnetAddr, selectSubnetIface, selectSubnetRelayLinkaddr, /// selectSubnetRelayInterfaceId. Note that the concept of interface-id is not diff --git a/src/bin/dhcp4/tests/wireshark.cc b/src/bin/dhcp4/tests/wireshark.cc index b563354d3b..80b4737083 100644 --- a/src/bin/dhcp4/tests/wireshark.cc +++ b/src/bin/dhcp4/tests/wireshark.cc @@ -100,6 +100,8 @@ Bootstrap Protocol Option: (55) Parameter Request List Option: (60) Vendor class identifier Option: (125) V-I Vendor-specific Information + - suboption 1 (Option Request): requesting option 2 + - suboption 5 (Modem Caps): 117 bytes Option: (43) Vendor-Specific Information (CableLabs) Option: (61) Client identifier Option: (57) Maximum DHCP Message Size diff --git a/src/bin/dhcp6/config_parser.cc b/src/bin/dhcp6/config_parser.cc index fdf0bae34a..0656297b5c 100644 --- a/src/bin/dhcp6/config_parser.cc +++ b/src/bin/dhcp6/config_parser.cc @@ -111,6 +111,13 @@ protected: << " for DHCPv4 server"); } + // Check if this is a vendor-option. If it is, get vendor-specific + // definition. + uint32_t vendor_id = SubnetConfigParser::optionSpaceToVendorId(option_space); + if (vendor_id) { + def = LibDHCP::getVendorOptionDef(Option::V6, vendor_id, option_code); + } + return def; } }; diff --git a/src/bin/dhcp6/dhcp6_srv.cc b/src/bin/dhcp6/dhcp6_srv.cc index 2b26c3b4cd..b3628500f2 100644 --- a/src/bin/dhcp6/dhcp6_srv.cc +++ b/src/bin/dhcp6/dhcp6_srv.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -683,6 +685,57 @@ Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) { } } +void +Dhcpv6Srv::appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) { + // Get the configured subnet suitable for the incoming packet. + Subnet6Ptr subnet = selectSubnet(question); + // 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) { + return; + } + + // Try to get the vendor option + boost::shared_ptr vendor_req = + boost::dynamic_pointer_cast(question->getOption(D6O_VENDOR_OPTS)); + if (!vendor_req) { + return; + } + + uint32_t vendor_id = vendor_req->getVendorId(); + + // Let's try to get ORO within that vendor-option + /// @todo This is very specific to vendor-id=4491 (Cable Labs). Other vendors + /// may have different policies. + boost::shared_ptr oro = + boost::dynamic_pointer_cast(vendor_req->getOption(DOCSIS3_V6_ORO)); + + // Option ORO not found. Don't do anything then. + if (!oro) { + return; + } + + boost::shared_ptr vendor_rsp(new OptionVendor(Option::V6, vendor_id)); + + // Get the list of options that client requested. + bool added = false; + const std::vector& requested_opts = oro->getValues(); + BOOST_FOREACH(uint16_t opt, requested_opts) { + Subnet::OptionDescriptor desc = subnet->getVendorOptionDescriptor(vendor_id, opt); + if (desc.option) { + vendor_rsp->addOption(desc.option); + added = true; + } + } + + if (added) { + answer->addOption(vendor_rsp); + } +} + OptionPtr Dhcpv6Srv::createStatusCode(uint16_t code, const std::string& text) { // @todo This function uses OptionCustom class to manage contents @@ -2114,6 +2167,7 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) { copyDefaultOptions(solicit, advertise); appendDefaultOptions(solicit, advertise); appendRequestedOptions(solicit, advertise); + appendRequestedVendorOptions(solicit, advertise); Option6ClientFqdnPtr fqdn = processClientFqdn(solicit); assignLeases(solicit, advertise, fqdn); @@ -2135,6 +2189,7 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) { copyDefaultOptions(request, reply); appendDefaultOptions(request, reply); appendRequestedOptions(request, reply); + appendRequestedVendorOptions(request, reply); Option6ClientFqdnPtr fqdn = processClientFqdn(request); assignLeases(request, reply, fqdn); @@ -2301,6 +2356,22 @@ Dhcpv6Srv::unpackOptions(const OptionBuffer& buf, continue; } + if (opt_type == D6O_VENDOR_OPTS) { + if (offset + 4 > length) { + // Truncated vendor-option. There is expected at least 4 bytes + // long enterprise-id field + return (offset); + } + + // Parse this as vendor option + OptionPtr vendor_opt(new OptionVendor(Option::V6, buf.begin() + offset, + buf.begin() + offset + opt_len)); + options.insert(std::make_pair(opt_type, vendor_opt)); + + offset += opt_len; + continue; + } + // Get all definitions with the particular option code. Note that option // code is non-unique within this container however at this point we // expect to get one option definition with the particular code. If more diff --git a/src/bin/dhcp6/dhcp6_srv.h b/src/bin/dhcp6/dhcp6_srv.h index 65edd779a3..577c075f26 100644 --- a/src/bin/dhcp6/dhcp6_srv.h +++ b/src/bin/dhcp6/dhcp6_srv.h @@ -336,6 +336,15 @@ protected: /// @param answer server's message (options will be added here) void appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer); + /// @brief Appends requested vendor options to server's answer. + /// + /// This is mostly useful for Cable Labs options for now, but the method + /// is easily extensible to other vendors. + /// + /// @param question client's message + /// @param answer server's message (vendor options will be added here) + void appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer); + /// @brief Assigns leases. /// /// It supports addresses (IA_NA) only. It does NOT support temporary diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc index acfc270ec1..c4b96caf1d 100644 --- a/src/bin/dhcp6/tests/config_parser_unittest.cc +++ b/src/bin/dhcp6/tests/config_parser_unittest.cc @@ -2049,6 +2049,118 @@ TEST_F(Dhcp6ParserTest, stdOptionData) { EXPECT_EQ(1516, optionIA->getT2()); } +// This test checks if vendor options can be specified in the config file +// (in hex format), and later retrieved from configured subnet +TEST_F(Dhcp6ParserTest, vendorOptionsHex) { + + // This configuration string is to configure two options + // sharing the code 1 and belonging to the different vendor spaces. + // (different vendor-id values). + string config = "{ \"interfaces\": [ \"*\" ]," + "\"rebind-timer\": 2000," + "\"renew-timer\": 1000," + "\"option-data\": [ {" + " \"name\": \"option-one\"," + " \"space\": \"vendor-4491\"," + " \"code\": 100," + " \"data\": \"AB CDEF0105\"," + " \"csv-format\": False" + " }," + " {" + " \"name\": \"option-two\"," + " \"space\": \"vendor-1234\"," + " \"code\": 100," + " \"data\": \"1234\"," + " \"csv-format\": False" + " } ]," + "\"subnet6\": [ { " + " \"pool\": [ \"2001:db8:1::/80\" ]," + " \"subnet\": \"2001:db8:1::/64\"" + " } ]" + "}"; + + ConstElementPtr status; + + ElementPtr json = Element::fromJSON(config); + + EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json)); + ASSERT_TRUE(status); + checkResult(status, 0); + + // Options should be now available for the subnet. + Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5")); + ASSERT_TRUE(subnet); + + // Try to get the option from the vendor space 4491 + Subnet::OptionDescriptor desc1 = subnet->getVendorOptionDescriptor(4491, 100); + ASSERT_TRUE(desc1.option); + EXPECT_EQ(100, desc1.option->getType()); + // Try to get the option from the vendor space 1234 + Subnet::OptionDescriptor desc2 = subnet->getVendorOptionDescriptor(1234, 100); + ASSERT_TRUE(desc2.option); + EXPECT_EQ(100, desc1.option->getType()); + + // Try to get the non-existing option from the non-existing + // option space and expect that option is not returned. + Subnet::OptionDescriptor desc3 = subnet->getVendorOptionDescriptor(5678, 38); + ASSERT_FALSE(desc3.option); +} + +// This test checks if vendor options can be specified in the config file, +// (in csv format), and later retrieved from configured subnet +TEST_F(Dhcp6ParserTest, vendorOptionsCsv) { + + // This configuration string is to configure two options + // sharing the code 1 and belonging to the different vendor spaces. + // (different vendor-id values). + string config = "{ \"interfaces\": [ \"*\" ]," + "\"rebind-timer\": 2000," + "\"renew-timer\": 1000," + "\"option-data\": [ {" + " \"name\": \"foo\"," + " \"space\": \"vendor-4491\"," + " \"code\": 100," + " \"data\": \"this is a string vendor-opt\"," + " \"csv-format\": True" + " } ]," + "\"option-def\": [ {" + " \"name\": \"foo\"," + " \"code\": 100," + " \"type\": \"string\"," + " \"array\": False," + " \"record-types\": \"\"," + " \"space\": \"vendor-4491\"," + " \"encapsulate\": \"\"" + " } ]," + "\"subnet6\": [ { " + " \"pool\": [ \"2001:db8:1::/80\" ]," + " \"subnet\": \"2001:db8:1::/64\"" + " } ]" + "}"; + + ConstElementPtr status; + + ElementPtr json = Element::fromJSON(config); + + EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json)); + ASSERT_TRUE(status); + checkResult(status, 0); + + // Options should be now available for the subnet. + Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5")); + ASSERT_TRUE(subnet); + + // Try to get the option from the vendor space 4491 + Subnet::OptionDescriptor desc1 = subnet->getVendorOptionDescriptor(4491, 100); + ASSERT_TRUE(desc1.option); + EXPECT_EQ(100, desc1.option->getType()); + + // Try to get the non-existing option from the non-existing + // option space and expect that option is not returned. + Subnet::OptionDescriptor desc2 = subnet->getVendorOptionDescriptor(5678, 38); + ASSERT_FALSE(desc2.option); +} + // The goal of this test is to verify that the standard option can // be configured to encapsulate multiple other options. TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) { diff --git a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc index ba8b8daffb..26a66c50d4 100644 --- a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc @@ -25,10 +25,13 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -2014,9 +2017,210 @@ TEST_F(Dhcpv6SrvTest, docsisTraffic) { ASSERT_FALSE(srv.fake_sent_.empty()); Pkt6Ptr adv = srv.fake_sent_.front(); ASSERT_TRUE(adv); +} + +// Checks if server is able to handle a relayed traffic from DOCSIS3.0 modems +TEST_F(Dhcpv6SrvTest, docsisVendorOptionsParse) { + + NakedDhcpv6Srv srv(0); + + // Let's get a traffic capture from DOCSIS3.0 modem + Pkt6Ptr sol = captureDocsisRelayedSolicit(); + EXPECT_NO_THROW(sol->unpack()); + + // Check if the packet contain + OptionPtr opt = sol->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(opt); + + boost::shared_ptr vendor = boost::dynamic_pointer_cast(opt); + ASSERT_TRUE(vendor); + + EXPECT_TRUE(vendor->getOption(1)); + EXPECT_TRUE(vendor->getOption(36)); + EXPECT_TRUE(vendor->getOption(35)); + EXPECT_TRUE(vendor->getOption(2)); + 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(10)); + 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(Dhcpv6SrvTest, docsisVendorORO) { + + NakedDhcpv6Srv srv(0); + + // Let's get a traffic capture from DOCSIS3.0 modem + Pkt6Ptr sol = captureDocsisRelayedSolicit(); + EXPECT_NO_THROW(sol->unpack()); + + // Check if the packet contain + OptionPtr opt = sol->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(opt); + + boost::shared_ptr vendor = boost::dynamic_pointer_cast(opt); + ASSERT_TRUE(vendor); + + opt = vendor->getOption(DOCSIS3_V6_ORO); + ASSERT_TRUE(opt); + + OptionUint16ArrayPtr oro = boost::dynamic_pointer_cast(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(Dhcpv6SrvTest, vendorOptionsORO) { + ConstElementPtr x; + string config = "{ \"interfaces\": [ \"all\" ]," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + " \"option-def\": [ {" + " \"name\": \"config-file\"," + " \"code\": 33," + " \"type\": \"string\"," + " \"array\": False," + " \"record-types\": \"\"," + " \"space\": \"vendor-4491\"," + " \"encapsulate\": \"\"" + " } ]," + " \"option-data\": [ {" + " \"name\": \"config-file\"," + " \"space\": \"vendor-4491\"," + " \"code\": 33," + " \"data\": \"normal_erouter_v6.cm\"," + " \"csv-format\": True" + " }]," + "\"subnet6\": [ { " + " \"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 }"; + + ElementPtr json = Element::fromJSON(config); + + NakedDhcpv6Srv srv(0); + + EXPECT_NO_THROW(x = configureDhcp6Server(srv, json)); + ASSERT_TRUE(x); + comment_ = parseAnswer(rcode_, x); + + ASSERT_EQ(0, rcode_); + + Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + sol->setRemoteAddr(IOAddress("fe80::abcd")); + sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + OptionPtr clientid = generateClientId(); + sol->addOption(clientid); + + // Pass it to the server and get an advertise + Pkt6Ptr adv = srv.processSolicit(sol); + + // check if we get response at all + ASSERT_TRUE(adv); + + // We did not include any vendor opts in SOLCIT, 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 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, 4491)); + vendor->addOption(vendor_oro); + sol->addOption(vendor); - /// @todo Check that the ADVERTISE is ok, that it includes all options, - /// that is relayed properly, etc. + // Need to process SOLICIT again after requesting new option. + adv = srv.processSolicit(sol); + ASSERT_TRUE(adv); + + // Check if thre is vendor option response + OptionPtr tmp = adv->getOption(D6O_VENDOR_OPTS); + ASSERT_TRUE(tmp); + + // The response should be OptionVendor object + boost::shared_ptr vendor_resp = + boost::dynamic_pointer_cast(tmp); + ASSERT_TRUE(vendor_resp); + + OptionPtr docsis33 = vendor_resp->getOption(33); + ASSERT_TRUE(docsis33); + + OptionStringPtr config_file = boost::dynamic_pointer_cast(docsis33); + ASSERT_TRUE(config_file); + EXPECT_EQ("normal_erouter_v6.cm", config_file->getValue()); +} + +// Test checks whether it is possible to use option definitions defined in +// src/lib/dhcp/docsis3_option_defs.h. +TEST_F(Dhcpv6SrvTest, vendorOptionsDocsisDefinitions) { + ConstElementPtr x; + string config_prefix = "{ \"interfaces\": [ \"all\" ]," + "\"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\": [ { " + " \"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; + + ElementPtr json_bogus = Element::fromJSON(config_bogus); + ElementPtr json_valid = Element::fromJSON(config_valid); + + NakedDhcpv6Srv srv(0); + + // This should fail (missing option definition) + EXPECT_NO_THROW(x = configureDhcp6Server(srv, json_bogus)); + ASSERT_TRUE(x); + comment_ = 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_ = parseAnswer(rcode_, x); + ASSERT_EQ(0, rcode_); } // This test verifies that the following option structure can be parsed: diff --git a/src/lib/dhcp/Makefile.am b/src/lib/dhcp/Makefile.am index 7402dbe1a2..316039b209 100644 --- a/src/lib/dhcp/Makefile.am +++ b/src/lib/dhcp/Makefile.am @@ -29,6 +29,7 @@ libb10_dhcp___la_SOURCES += option6_iaaddr.cc option6_iaaddr.h libb10_dhcp___la_SOURCES += option6_iaprefix.cc option6_iaprefix.h libb10_dhcp___la_SOURCES += option6_addrlst.cc option6_addrlst.h libb10_dhcp___la_SOURCES += option6_client_fqdn.cc option6_client_fqdn.h +libb10_dhcp___la_SOURCES += option_vendor.cc option_vendor.h libb10_dhcp___la_SOURCES += option_int.h libb10_dhcp___la_SOURCES += option_int_array.h libb10_dhcp___la_SOURCES += option.cc option.h diff --git a/src/lib/dhcp/docsis3_option_defs.h b/src/lib/dhcp/docsis3_option_defs.h new file mode 100644 index 0000000000..9707e5dc6f --- /dev/null +++ b/src/lib/dhcp/docsis3_option_defs.h @@ -0,0 +1,65 @@ +// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef DOCSIS3_OPTION_DEFS_H +#define DOCSIS3_OPTION_DEFS_H + +#include +#include + + +namespace { + +#define VENDOR_ID_CABLE_LABS 4491 + +#define DOCSIS3_V4_ORO 1 +#define DOCSIS3_V4_TFTP_SERVERS 2 + +/// @brief Definitions of standard DHCPv4 options. +const OptionDefParams DOCSIS3_V4_DEFS[] = { + { "oro", DOCSIS3_V4_ORO, OPT_UINT8_TYPE, true, NO_RECORD_DEF, "" }, + { "tftp-servers", DOCSIS3_V4_TFTP_SERVERS, OPT_IPV4_ADDRESS_TYPE, true, NO_RECORD_DEF, "" } +}; + +/// Number of option definitions defined. +const int DOCSIS3_V4_DEFS_SIZE = sizeof(DOCSIS3_V4_DEFS) / sizeof(OptionDefParams); + +#define DOCSIS3_V6_ORO 1 +#define DOCSIS3_V6_DEVICE_TYPE 2 +#define DOCSIS3_V6_VENDOR_NAME 10 +#define DOCSIS3_V6_TFTP_SERVERS 32 +#define DOCSIS3_V6_CONFIG_FILE 33 +#define DOCSIS3_V6_SYSLOG_SERVERS 34 +#define DOCSIS3_V6_TIME_SERVERS 37 +#define DOCSIS3_V6_TIME_OFFSET 38 + +/// @brief Definitions of standard DHCPv6 options. +const OptionDefParams DOCSIS3_V6_DEFS[] = { + { "oro", DOCSIS3_V6_ORO, OPT_UINT16_TYPE, true, NO_RECORD_DEF, "" }, + { "device-type", DOCSIS3_V6_DEVICE_TYPE, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" }, + { "vendor-type", DOCSIS3_V6_VENDOR_NAME, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" }, + { "tftp-servers", DOCSIS3_V6_TFTP_SERVERS, OPT_IPV6_ADDRESS_TYPE, true, NO_RECORD_DEF, "" }, + { "time-servers", DOCSIS3_V6_TIME_SERVERS, OPT_IPV6_ADDRESS_TYPE, true, NO_RECORD_DEF, "" }, + { "config-file", DOCSIS3_V6_CONFIG_FILE, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" }, + { "syslog-servers", DOCSIS3_V6_SYSLOG_SERVERS, OPT_IPV6_ADDRESS_TYPE, true, NO_RECORD_DEF, "" }, + { "time-offset", DOCSIS3_V6_TIME_OFFSET, OPT_INT32_TYPE, false, NO_RECORD_DEF, "" } + // @todo add definitions for all remaning options. +}; + +/// Number of option definitions defined. +const int DOCSIS3_V6_DEFS_SIZE = sizeof(DOCSIS3_V6_DEFS) / sizeof(OptionDefParams); + +}; // anonymous namespace + +#endif // STD_OPTION_DEFS_H diff --git a/src/lib/dhcp/libdhcp++.cc b/src/lib/dhcp/libdhcp++.cc index 351fe8ca83..524d56adf3 100644 --- a/src/lib/dhcp/libdhcp++.cc +++ b/src/lib/dhcp/libdhcp++.cc @@ -18,13 +18,16 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include +#include #include #include @@ -45,17 +48,29 @@ OptionDefContainer LibDHCP::v4option_defs_; // Static container with DHCPv6 option definitions. OptionDefContainer LibDHCP::v6option_defs_; +VendorOptionDefContainers LibDHCP::vendor4_defs_; + +VendorOptionDefContainers LibDHCP::vendor6_defs_; + +// Let's keep it in .cc file. Moving it to .h would require including optionDefParams +// definitions there +void initOptionSpace(OptionDefContainer& defs, + const OptionDefParams* params, + size_t params_size); + const OptionDefContainer& LibDHCP::getOptionDefs(const Option::Universe u) { switch (u) { case Option::V4: if (v4option_defs_.empty()) { initStdOptionDefs4(); + initVendorOptsDocsis4(); } return (v4option_defs_); case Option::V6: if (v6option_defs_.empty()) { initStdOptionDefs6(); + initVendorOptsDocsis6(); } return (v6option_defs_); default: @@ -63,6 +78,38 @@ LibDHCP::getOptionDefs(const Option::Universe u) { } } +const OptionDefContainer* +LibDHCP::getVendorOption4Defs(uint32_t vendor_id) { + + if (vendor_id == VENDOR_ID_CABLE_LABS && + vendor4_defs_.find(VENDOR_ID_CABLE_LABS) == vendor4_defs_.end()) { + initVendorOptsDocsis4(); + } + + VendorOptionDefContainers::const_iterator def = vendor4_defs_.find(vendor_id); + if (def == vendor4_defs_.end()) { + // No such vendor-id space + return (NULL); + } + return (&(def->second)); +} + +const OptionDefContainer* +LibDHCP::getVendorOption6Defs(uint32_t vendor_id) { + + if (vendor_id == VENDOR_ID_CABLE_LABS && + vendor6_defs_.find(VENDOR_ID_CABLE_LABS) == vendor6_defs_.end()) { + initVendorOptsDocsis6(); + } + + VendorOptionDefContainers::const_iterator def = vendor6_defs_.find(vendor_id); + if (def == vendor6_defs_.end()) { + // No such vendor-id space + return (NULL); + } + return (&(def->second)); +} + OptionDefinitionPtr LibDHCP::getOptionDef(const Option::Universe u, const uint16_t code) { const OptionDefContainer& defs = getOptionDefs(u); @@ -74,6 +121,31 @@ LibDHCP::getOptionDef(const Option::Universe u, const uint16_t code) { return (OptionDefinitionPtr()); } +OptionDefinitionPtr +LibDHCP::getVendorOptionDef(const Option::Universe u, const uint32_t vendor_id, + const uint16_t code) { + const OptionDefContainer* defs = NULL; + if (u == Option::V4) { + defs = getVendorOption4Defs(vendor_id); + } else if (u == Option::V6) { + defs = getVendorOption6Defs(vendor_id); + } + + if (!defs) { + // Weird universe or unknown vendor_id. We don't care. No definitions + // one way or another + // What is it anyway? + return (OptionDefinitionPtr()); + } + + const OptionDefContainerTypeIndex& idx = defs->get<1>(); + const OptionDefContainerTypeRange& range = idx.equal_range(code); + if (range.first != range.second) { + return (*range.first); + } + return (OptionDefinitionPtr()); +} + bool LibDHCP::isStandardOption(const Option::Universe u, const uint16_t code) { if (u == Option::V6) { @@ -164,6 +236,23 @@ size_t LibDHCP::unpackOptions6(const OptionBuffer& buf, continue; } + if (opt_type == D6O_VENDOR_OPTS) { + if (offset + 4 > length) { + // Truncated vendor-option. There is expected at least 4 bytes + // long enterprise-id field + return (offset); + } + + // Parse this as vendor option + OptionPtr vendor_opt(new OptionVendor(Option::V6, buf.begin() + offset, + buf.begin() + offset + opt_len)); + options.insert(std::make_pair(opt_type, vendor_opt)); + + offset += opt_len; + continue; + } + + // Get all definitions with the particular option code. Note that option // code is non-unique within this container however at this point we // expect to get one option definition with the particular code. If more @@ -280,6 +369,191 @@ size_t LibDHCP::unpackOptions4(const OptionBuffer& buf, return (offset); } +size_t LibDHCP::unpackVendorOptions6(uint32_t vendor_id, + const OptionBuffer& buf, + isc::dhcp::OptionCollection& options) { + size_t offset = 0; + size_t length = buf.size(); + + // Get the list of option definitions for this particular vendor-id + const OptionDefContainer* option_defs = LibDHCP::getVendorOption6Defs(vendor_id); + + // Get the search index #1. It allows to search for option definitions + // using option code. If there's no such vendor-id space, we're out of luck + // anyway. + const OptionDefContainerTypeIndex* idx = NULL; + if (option_defs) { + idx = &(option_defs->get<1>()); + } + + // The buffer being read comprises a set of options, each starting with + // a two-byte type code and a two-byte length field. + while (offset + 4 <= length) { + uint16_t opt_type = isc::util::readUint16(&buf[offset]); + offset += 2; + + uint16_t opt_len = isc::util::readUint16(&buf[offset]); + offset += 2; + + if (offset + opt_len > length) { + // @todo: consider throwing exception here. + return (offset); + } + + OptionPtr opt; + opt.reset(); + + // If there is a definition for such a vendor option... + if (idx) { + // Get all definitions with the particular option code. Note that option + // code is non-unique within this container however at this point we + // expect to get one option definition with the particular code. If more + // are returned we report an error. + const OptionDefContainerTypeRange& range = idx->equal_range(opt_type); + // Get the number of returned option definitions for the option code. + size_t num_defs = distance(range.first, range.second); + + if (num_defs > 1) { + // Multiple options of the same code are not supported right now! + isc_throw(isc::Unexpected, "Internal error: multiple option definitions" + " for option type " << opt_type << " returned. Currently it is not" + " supported to initialize multiple option definitions" + " for the same option code. This will be supported once" + " support for option spaces is implemented"); + } else if (num_defs == 1) { + // The option definition has been found. Use it to create + // the option instance from the provided buffer chunk. + const OptionDefinitionPtr& def = *(range.first); + assert(def); + opt = def->optionFactory(Option::V6, opt_type, + buf.begin() + offset, + buf.begin() + offset + opt_len); + } + } + + // This can happen in one of 2 cases: + // 1. we do not have definitions for that vendor-space + // 2. we do have definitions, but that particular option was not defined + if (!opt) { + opt = OptionPtr(new Option(Option::V6, opt_type, + buf.begin() + offset, + buf.begin() + offset + opt_len)); + } + + // add option to options + if (opt) { + options.insert(std::make_pair(opt_type, opt)); + } + offset += opt_len; + } + + return (offset); +} + +size_t LibDHCP::unpackVendorOptions4(uint32_t vendor_id, const OptionBuffer& buf, + isc::dhcp::OptionCollection& options) { + size_t offset = 0; + + // Get the list of stdandard option definitions. + const OptionDefContainer* option_defs = LibDHCP::getVendorOption4Defs(vendor_id); + // Get the search index #1. It allows to search for option definitions + // using option code. + const OptionDefContainerTypeIndex* idx = NULL; + if (option_defs) { + idx = &(option_defs->get<1>()); + } + + // The buffer being read comprises a set of options, each starting with + // a one-byte type code and a one-byte length field. + while (offset + 1 <= buf.size()) { + + // Note that Vendor-Specific info option (RFC3925) has a different option + // format than Vendor-Spec info for DHCPv6. (there's additional layer of + // data-length + uint8_t data_len = buf[offset++]; + + if (offset + data_len > buf.size()) { + // Truncated data-option + return (offset); + } + + uint8_t offset_end = offset + data_len; + + // beginning of data-chunk parser + while (offset + 1 <= offset_end) { + uint8_t opt_type = buf[offset++]; + + // DHO_END is a special, one octet long option + if (opt_type == DHO_END) + return (offset); // just return. Don't need to add DHO_END option + + // DHO_PAD is just a padding after DHO_END. Let's continue parsing + // in case we receive a message without DHO_END. + if (opt_type == DHO_PAD) + continue; + + if (offset + 1 >= buf.size()) { + // opt_type must be cast to integer so as it is not treated as + // unsigned char value (a number is presented in error message). + isc_throw(OutOfRange, "Attempt to parse truncated option " + << static_cast(opt_type)); + } + + uint8_t opt_len = buf[offset++]; + if (offset + opt_len > buf.size()) { + isc_throw(OutOfRange, "Option parse failed. Tried to parse " + << offset + opt_len << " bytes from " << buf.size() + << "-byte long buffer."); + } + + OptionPtr opt; + opt.reset(); + + if (idx) { + // Get all definitions with the particular option code. Note that option code + // is non-unique within this container however at this point we expect + // to get one option definition with the particular code. If more are + // returned we report an error. + const OptionDefContainerTypeRange& range = idx->equal_range(opt_type); + // Get the number of returned option definitions for the option code. + size_t num_defs = distance(range.first, range.second); + + if (num_defs > 1) { + // Multiple options of the same code are not supported right now! + isc_throw(isc::Unexpected, "Internal error: multiple option definitions" + " for option type " << static_cast(opt_type) + << " returned. Currently it is not supported to initialize" + << " multiple option definitions for the same option code." + << " This will be supported once support for option spaces" + << " is implemented"); + } else if (num_defs == 1) { + // The option definition has been found. Use it to create + // the option instance from the provided buffer chunk. + const OptionDefinitionPtr& def = *(range.first); + assert(def); + opt = def->optionFactory(Option::V4, opt_type, + buf.begin() + offset, + buf.begin() + offset + opt_len); + } + } + + if (!opt) { + opt = OptionPtr(new Option(Option::V4, opt_type, + buf.begin() + offset, + buf.begin() + offset + opt_len)); + } + + options.insert(std::make_pair(opt_type, opt)); + offset += opt_len; + + } // end of data-chunk + + } + return (offset); +} + + + void LibDHCP::packOptions(isc::util::OutputBuffer& buf, const OptionCollection& options) { @@ -385,13 +659,30 @@ LibDHCP::initStdOptionDefs4() { void LibDHCP::initStdOptionDefs6() { - v6option_defs_.clear(); + initOptionSpace(v6option_defs_, OPTION_DEF_PARAMS6, OPTION_DEF_PARAMS_SIZE6); +} - for (int i = 0; i < OPTION_DEF_PARAMS_SIZE6; ++i) { - std::string encapsulates(OPTION_DEF_PARAMS6[i].encapsulates); - if (!encapsulates.empty() && OPTION_DEF_PARAMS6[i].array) { +void +LibDHCP::initVendorOptsDocsis4() { + initOptionSpace(vendor4_defs_[VENDOR_ID_CABLE_LABS], DOCSIS3_V4_DEFS, DOCSIS3_V4_DEFS_SIZE); +} + +void +LibDHCP::initVendorOptsDocsis6() { + vendor6_defs_[VENDOR_ID_CABLE_LABS] = OptionDefContainer(); + initOptionSpace(vendor6_defs_[VENDOR_ID_CABLE_LABS], DOCSIS3_V6_DEFS, DOCSIS3_V6_DEFS_SIZE); +} + +void initOptionSpace(OptionDefContainer& defs, + const OptionDefParams* params, + size_t params_size) { + defs.clear(); + + for (int i = 0; i < params_size; ++i) { + std::string encapsulates(params[i].encapsulates); + if (!encapsulates.empty() && params[i].array) { isc_throw(isc::BadValue, "invalid standard option definition: " - << "option with code '" << OPTION_DEF_PARAMS6[i].code + << "option with code '" << params[i].code << "' may not encapsulate option space '" << encapsulates << "' because the definition" << " indicates that this option comprises an array" @@ -404,33 +695,33 @@ LibDHCP::initStdOptionDefs6() { OptionDefinitionPtr definition; if (encapsulates.empty()) { // Option does not encapsulate any option space. - definition.reset(new OptionDefinition(OPTION_DEF_PARAMS6[i].name, - OPTION_DEF_PARAMS6[i].code, - OPTION_DEF_PARAMS6[i].type, - OPTION_DEF_PARAMS6[i].array)); + definition.reset(new OptionDefinition(params[i].name, + params[i].code, + params[i].type, + params[i].array)); } else { // Option does encapsulate an option space. - definition.reset(new OptionDefinition(OPTION_DEF_PARAMS6[i].name, - OPTION_DEF_PARAMS6[i].code, - OPTION_DEF_PARAMS6[i].type, - OPTION_DEF_PARAMS6[i].encapsulates)); + definition.reset(new OptionDefinition(params[i].name, + params[i].code, + params[i].type, + params[i].encapsulates)); } - for (int rec = 0; rec < OPTION_DEF_PARAMS6[i].records_size; ++rec) { - definition->addRecordField(OPTION_DEF_PARAMS6[i].records[rec]); + for (int rec = 0; rec < params[i].records_size; ++rec) { + definition->addRecordField(params[i].records[rec]); } try { definition->validate(); - } catch (const Exception& ex) { + } catch (const isc::Exception& ex) { // This is unlikely event that validation fails and may // be only caused by programming error. To guarantee the // data consistency we clear all option definitions that // have been added so far and pass the exception forward. - v6option_defs_.clear(); + defs.clear(); throw; } - v6option_defs_.push_back(definition); + defs.push_back(definition); } } diff --git a/src/lib/dhcp/libdhcp++.h b/src/lib/dhcp/libdhcp++.h index 71cf3f4969..4931746cb4 100644 --- a/src/lib/dhcp/libdhcp++.h +++ b/src/lib/dhcp/libdhcp++.h @@ -55,6 +55,11 @@ public: static OptionDefinitionPtr getOptionDef(const Option::Universe u, const uint16_t code); + + static OptionDefinitionPtr getVendorOptionDef(const Option::Universe u, + const uint32_t vendor_id, + const uint16_t code); + /// @brief Check if the specified option is a standard option. /// /// @param u universe (V4 or V6) @@ -149,6 +154,19 @@ public: uint16_t type, Option::Factory * factory); + static const OptionDefContainer* + getVendorOption4Defs(uint32_t vendor_id); + + static const OptionDefContainer* + getVendorOption6Defs(uint32_t vendor_id); + + static size_t unpackVendorOptions6(uint32_t vendor_id, + const OptionBuffer& buf, + isc::dhcp::OptionCollection& options); + + static size_t unpackVendorOptions4(uint32_t vendor_id, const OptionBuffer& buf, + isc::dhcp::OptionCollection& options); + private: /// Initialize standard DHCPv4 option definitions. @@ -170,6 +188,10 @@ private: /// is incorrect. This is a programming error. static void initStdOptionDefs6(); + static void initVendorOptsDocsis4(); + + static void initVendorOptsDocsis6(); + /// pointers to factories that produce DHCPv6 options static FactoryMap v4factories_; @@ -181,6 +203,10 @@ private: /// Container with DHCPv6 option definitions. static OptionDefContainer v6option_defs_; + + static VendorOptionDefContainers vendor4_defs_; + + static VendorOptionDefContainers vendor6_defs_; }; } diff --git a/src/lib/dhcp/option_definition.h b/src/lib/dhcp/option_definition.h index 878abeb35f..29fca63976 100644 --- a/src/lib/dhcp/option_definition.h +++ b/src/lib/dhcp/option_definition.h @@ -23,6 +23,7 @@ #include #include #include +#include namespace isc { namespace dhcp { @@ -601,6 +602,9 @@ typedef boost::multi_index_container< /// Pointer to an option definition container. typedef boost::shared_ptr OptionDefContainerPtr; +/// Container that holds various vendor option containers +typedef std::map VendorOptionDefContainers; + /// Type of the index #1 - option type. typedef OptionDefContainer::nth_index<1>::type OptionDefContainerTypeIndex; /// Pair of iterators to represent the range of options definitions diff --git a/src/lib/dhcp/option_vendor.cc b/src/lib/dhcp/option_vendor.cc new file mode 100644 index 0000000000..22db1aff02 --- /dev/null +++ b/src/lib/dhcp/option_vendor.cc @@ -0,0 +1,68 @@ +// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include +#include +#include + +using namespace isc::dhcp; + +OptionVendor::OptionVendor(Option::Universe u, uint32_t vendor_id) + :Option(u, u==Option::V4?DHO_VIVSO_SUBOPTIONS:D6O_VENDOR_OPTS), vendor_id_(vendor_id) { +} + +OptionVendor::OptionVendor(Option::Universe u, OptionBufferConstIter begin, + OptionBufferConstIter end) + :Option(u, u==Option::V4?DHO_VIVSO_SUBOPTIONS:D6O_VENDOR_OPTS), vendor_id_(0) { + unpack(begin, end); +} + + +void OptionVendor::pack(isc::util::OutputBuffer& buf) { + packHeader(buf); + + buf.writeUint32(vendor_id_); + + packOptions(buf); +} + +void OptionVendor::unpack(OptionBufferConstIter begin, OptionBufferConstIter end) { + if (distance(begin, end) < sizeof(uint32_t)) { + isc_throw(OutOfRange, "Truncated vendor-specific information option"); + } + + vendor_id_ = isc::util::readUint32(&(*begin)); + + OptionBuffer vendor_buffer(begin +4, end); + + if (universe_ == Option::V6) { + LibDHCP::unpackVendorOptions6(vendor_id_, vendor_buffer, options_); + } else { + LibDHCP::unpackVendorOptions4(vendor_id_, vendor_buffer, options_); + } +} + +uint16_t OptionVendor::len() { + uint16_t length = getHeaderLen(); + + length += sizeof(uint32_t); // Vendor-id field + + // length of all suboptions + for (OptionCollection::iterator it = options_.begin(); + it != options_.end(); + ++it) { + length += (*it).second->len(); + } + return (length); +} diff --git a/src/lib/dhcp/option_vendor.h b/src/lib/dhcp/option_vendor.h new file mode 100644 index 0000000000..15ac2d2ef2 --- /dev/null +++ b/src/lib/dhcp/option_vendor.h @@ -0,0 +1,104 @@ +// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#ifndef OPTION_VENDOR_H +#define OPTION_VENDOR_H + +#include +#include +#include +#include + +#include + +namespace isc { +namespace dhcp { + +/// This class represents vendor-specific information option. +class OptionVendor: public Option { + +public: + /// @brief Constructor. + /// + /// @param u universe (V4 or V6) + /// @param vendor_id vendor enterprise-id (unique 32 bit integer) + OptionVendor(Option::Universe u, uint32_t vendor_id); + + /// @brief Constructor. + /// + /// This constructor creates option from a buffer. This constructor + /// may throw exception if \ref unpack function throws during buffer + /// parsing. + /// + /// @param u universe (V4 or V6) + /// @param begin iterator to first byte of option data. + /// @param end iterator to end of option data (first byte after option end). + /// + /// @throw isc::OutOfRange if provided buffer is shorter than data size. + /// @throw isc::dhcp::InvalidDataType if data field type provided + /// as template parameter is not a supported integer type. + /// @todo Extend constructor to set encapsulated option space name. + OptionVendor(Option::Universe u, OptionBufferConstIter begin, + OptionBufferConstIter end); + + /// Writes option in wire-format to buf, returns pointer to first unused + /// byte after stored option. + /// + /// @param [out] buf buffer (option will be stored here) + /// + /// @throw isc::dhcp::InvalidDataType if size of a data field type is not + /// equal to 1, 2 or 4 bytes. The data type is not checked in this function + /// because it is checked in a constructor. + void pack(isc::util::OutputBuffer& buf); + + /// @brief Parses received buffer + /// + /// Parses received buffer and returns offset to the first unused byte after + /// parsed option. + /// + /// @param begin iterator to first byte of option data + /// @param end iterator to end of option data (first byte after option end) + /// + /// @throw isc::OutOfRange if provided buffer is shorter than data size. + /// @throw isc::dhcp::InvalidDataType if size of a data field type is not + /// equal to 1, 2 or 4 bytes. The data type is not checked in this function + /// because it is checked in a constructor. + virtual void unpack(OptionBufferConstIter begin, OptionBufferConstIter end); + + /// @brief Set option value. + /// + /// @param value new option value. + void setVendorId(uint32_t vendor_id) { vendor_id_ = vendor_id; } + + /// @brief Return option value. + /// + /// @return option value. + uint32_t getVendorId() const { return vendor_id_; } + + /// @brief returns complete length of option + /// + /// Returns length of this option, including option header and suboptions + /// + /// @return length of this option + virtual uint16_t len(); + +private: + + uint32_t vendor_id_; ///< Enterprise-id +}; + +} // isc::dhcp namespace +} // isc namespace + +#endif // OPTION_INT_H diff --git a/src/lib/dhcp/std_option_defs.h b/src/lib/dhcp/std_option_defs.h index 526c80ad66..c330ff677c 100644 --- a/src/lib/dhcp/std_option_defs.h +++ b/src/lib/dhcp/std_option_defs.h @@ -16,6 +16,8 @@ #define STD_OPTION_DEFS_H #include +#include +#include namespace { diff --git a/src/lib/dhcpsrv/cfgmgr.h b/src/lib/dhcpsrv/cfgmgr.h index 0ec51d02bc..a589284d07 100644 --- a/src/lib/dhcpsrv/cfgmgr.h +++ b/src/lib/dhcpsrv/cfgmgr.h @@ -355,7 +355,7 @@ private: /// A collection of option definitions that can be accessed /// using option space name they belong to. OptionSpaceContainer option_def_spaces_; + OptionDefinitionPtr, std::string> option_def_spaces_; /// @brief Container for defined DHCPv6 option spaces. OptionSpaceCollection spaces6_; diff --git a/src/lib/dhcpsrv/dhcp_parsers.cc b/src/lib/dhcpsrv/dhcp_parsers.cc index e03d13f41d..7ace4e48f4 100644 --- a/src/lib/dhcpsrv/dhcp_parsers.cc +++ b/src/lib/dhcpsrv/dhcp_parsers.cc @@ -429,6 +429,8 @@ OptionDataParser::createOption() { << "')"); } + const bool csv_format = boolean_values_->getParam("csv-format"); + // Find the Option Definition for the option by its option code. // findOptionDefinition will throw if not found, no need to test. OptionDefinitionPtr def; @@ -449,7 +451,10 @@ OptionDataParser::createOption() { if (std::distance(range.first, range.second) > 0) { def = *range.first; } - if (!def) { + + // It's ok if we don't have option format if the option is + // specified as hex + if (!def && csv_format) { isc_throw(DhcpConfigError, "definition for the option '" << option_space << "." << option_name << "' having code '" << option_code @@ -459,7 +464,6 @@ OptionDataParser::createOption() { // Get option data from the configuration database ('data' field). const std::string option_data = string_values_->getParam("data"); - const bool csv_format = boolean_values_->getParam("csv-format"); // Transform string of hexadecimal digits into binary format. std::vector binary; @@ -1047,8 +1051,16 @@ SubnetConfigParser::createSubnet() { } // Add sub-options (if any). appendSubOptions(option_space, desc.option); - // In any case, we add the option to the subnet. - subnet_->addOption(desc.option, false, option_space); + + // thomson + uint32_t vendor_id = optionSpaceToVendorId(option_space); + if (vendor_id) { + // This is a vendor option + subnet_->addVendorOption(desc.option, false, vendor_id); + } else { + // This is a normal option + subnet_->addOption(desc.option, false, option_space); + } } } @@ -1077,12 +1089,59 @@ SubnetConfigParser::createSubnet() { if (!existing_desc.option) { // Add sub-options (if any). appendSubOptions(option_space, desc.option); - subnet_->addOption(desc.option, false, option_space); + + uint32_t vendor_id = optionSpaceToVendorId(option_space); + if (vendor_id) { + // This is a vendor option + subnet_->addVendorOption(desc.option, false, vendor_id); + } else { + // This is a normal option + subnet_->addOption(desc.option, false, option_space); + } } } } } +uint32_t +SubnetConfigParser::optionSpaceToVendorId(const std::string& option_space) { + if (option_space.size() < 8) { + // 8 is a minimal length of "vendor-X" format + return (0); + } + if (option_space.substr(0,7) != "vendor-") { + return (0); + } + + // text after "vendor-", supposedly numbers only + string x = option_space.substr(7); + + int64_t check; + try { + check = boost::lexical_cast(x); + } catch (const boost::bad_lexical_cast &) { + /// @todo: Should we throw here? + // isc_throw(BadValue, "Failed to parse vendor-X value (" << x + // << ") as unsigned 32-bit integer."); + return (0); + } + if (check > std::numeric_limits::max()) { + /// @todo: Should we throw here? + //isc_throw(BadValue, "Value " << x << "is too large" + // << " for unsigned 32-bit integer."); + return (0); + } + if (check < 0) { + /// @todo: Should we throw here? + // isc_throw(BadValue, "Value " << x << "is negative." + // << " Only 0 or larger are allowed for unsigned 32-bit integer."); + return (0); + } + + // value is small enough to fit + return (static_cast(check)); +} + isc::dhcp::Triplet SubnetConfigParser::getParam(const std::string& name) { uint32_t value = 0; diff --git a/src/lib/dhcpsrv/dhcp_parsers.h b/src/lib/dhcpsrv/dhcp_parsers.h index 0829b21c2b..738c556279 100644 --- a/src/lib/dhcpsrv/dhcp_parsers.h +++ b/src/lib/dhcpsrv/dhcp_parsers.h @@ -34,14 +34,14 @@ namespace dhcp { /// @brief Storage for option definitions. typedef OptionSpaceContainer OptionDefStorage; + OptionDefinitionPtr, std::string> OptionDefStorage; /// @brief Shared pointer to option definitions storage. typedef boost::shared_ptr OptionDefStoragePtr; /// Collection of containers holding option spaces. Each container within /// a particular option space holds so-called option descriptors. typedef OptionSpaceContainer OptionStorage; + Subnet::OptionDescriptor, std::string> OptionStorage; /// @brief Shared pointer to option storage. typedef boost::shared_ptr OptionStoragePtr; @@ -773,6 +773,14 @@ public: /// @brief Adds the created subnet to a server's configuration. virtual void commit() = 0; + /// @brief tries to convert option_space string to numeric vendor_id + /// + /// This will work if the option_space has format "vendor-1234". + /// This is used to detect whether a given option-space is a vendor + /// space or not. Returns 0 if the format is different. + /// @return numeric vendor-id (or 0 if the format does not match) + static uint32_t optionSpaceToVendorId(const std::string& option_space); + protected: /// @brief creates parsers for entries in subnet definition /// diff --git a/src/lib/dhcpsrv/option_space_container.h b/src/lib/dhcpsrv/option_space_container.h index ba16fbb02d..641720754d 100644 --- a/src/lib/dhcpsrv/option_space_container.h +++ b/src/lib/dhcpsrv/option_space_container.h @@ -31,7 +31,8 @@ namespace dhcp { /// @tparam ContainerType of the container holding items within /// option space. /// @tparam ItemType type of the item being held by the container. -template +/// @tparam Selector a string (for option spaces) or uint32_t (for vendor options) +template class OptionSpaceContainer { public: @@ -42,7 +43,7 @@ public: /// /// @param item reference to the item being added. /// @param option_space name of the option space. - void addItem(const ItemType& item, const std::string& option_space) { + void addItem(const ItemType& item, const Selector& option_space) { ItemsContainerPtr items = getItems(option_space); items->push_back(item); option_space_map_[option_space] = items; @@ -57,7 +58,7 @@ public: /// @param option_space name of the option space. /// /// @return pointer to the container holding items. - ItemsContainerPtr getItems(const std::string& option_space) const { + ItemsContainerPtr getItems(const Selector& option_space) const { const typename OptionSpaceMap::const_iterator& items = option_space_map_.find(option_space); if (items == option_space_map_.end()) { @@ -73,8 +74,8 @@ public: /// @todo This function is likely to be removed once /// we create a structore of OptionSpaces defined /// through the configuration manager. - std::list getOptionSpaceNames() { - std::list names; + std::list getOptionSpaceNames() { + std::list names; for (typename OptionSpaceMap::const_iterator space = option_space_map_.begin(); space != option_space_map_.end(); ++space) { @@ -91,7 +92,7 @@ public: private: /// A map holding container (option space name is the key). - typedef std::map OptionSpaceMap; + typedef std::map OptionSpaceMap; OptionSpaceMap option_space_map_; }; diff --git a/src/lib/dhcpsrv/subnet.cc b/src/lib/dhcpsrv/subnet.cc index 4320733434..d01fb3d74b 100644 --- a/src/lib/dhcpsrv/subnet.cc +++ b/src/lib/dhcpsrv/subnet.cc @@ -88,6 +88,38 @@ Subnet::getOptionDescriptor(const std::string& option_space, return (*range.first); } +void Subnet::addVendorOption(const OptionPtr& option, bool persistent, + uint32_t vendor_id){ + + validateOption(option); + + vendor_option_spaces_.addItem(OptionDescriptor(option, persistent), vendor_id); +} + +Subnet::OptionContainerPtr +Subnet::getVendorOptionDescriptors(uint32_t vendor_id) const { + return (vendor_option_spaces_.getItems(vendor_id)); +} + +Subnet::OptionDescriptor +Subnet::getVendorOptionDescriptor(uint32_t vendor_id, uint16_t option_code) { + OptionContainerPtr options = getVendorOptionDescriptors(vendor_id); + if (!options || options->empty()) { + return (OptionDescriptor(false)); + } + const OptionContainerTypeIndex& idx = options->get<1>(); + const OptionContainerTypeRange& range = idx.equal_range(option_code); + if (std::distance(range.first, range.second) == 0) { + return (OptionDescriptor(false)); + } + + return (*range.first); +} + +void Subnet::delVendorOptions() { + vendor_option_spaces_.clearItems(); +} + isc::asiolink::IOAddress Subnet::getLastAllocated(Lease::Type type) const { // check if the type is valid (and throw if it isn't) checkType(type); diff --git a/src/lib/dhcpsrv/subnet.h b/src/lib/dhcpsrv/subnet.h index ac6de0332d..f6714d1b3f 100644 --- a/src/lib/dhcpsrv/subnet.h +++ b/src/lib/dhcpsrv/subnet.h @@ -180,9 +180,14 @@ public: void addOption(const OptionPtr& option, bool persistent, const std::string& option_space); + void addVendorOption(const OptionPtr& option, bool persistent, + uint32_t vendor_id); + /// @brief Delete all options configured for the subnet. void delOptions(); + void delVendorOptions(); + /// @brief checks if the specified address is in pools /// /// Note the difference between inSubnet() and inPool(). For a given @@ -221,6 +226,9 @@ public: OptionContainerPtr getOptionDescriptors(const std::string& option_space) const; + OptionContainerPtr + getVendorOptionDescriptors(uint32_t vendor_id) const; + /// @brief Return single option descriptor. /// /// @param option_space name of the option space. @@ -232,6 +240,9 @@ public: getOptionDescriptor(const std::string& option_space, const uint16_t option_code); + OptionDescriptor + getVendorOptionDescriptor(uint32_t vendor_id, uint16_t option_code); + /// @brief returns the last address that was tried from this pool /// /// This method returns the last address that was attempted to be allocated @@ -440,9 +451,14 @@ private: /// A collection of option spaces grouping option descriptors. typedef OptionSpaceContainer OptionSpaceCollection; + OptionDescriptor, std::string> OptionSpaceCollection; + + typedef OptionSpaceContainer VendorOptionSpaceCollection; + OptionSpaceCollection option_spaces_; + VendorOptionSpaceCollection vendor_option_spaces_; }; /// @brief A generic pointer to either Subnet4 or Subnet6 object diff --git a/src/lib/dhcpsrv/tests/subnet_unittest.cc b/src/lib/dhcpsrv/tests/subnet_unittest.cc index 0af8191035..f872ed9083 100644 --- a/src/lib/dhcpsrv/tests/subnet_unittest.cc +++ b/src/lib/dhcpsrv/tests/subnet_unittest.cc @@ -597,6 +597,76 @@ TEST(Subnet6Test, getOptionDescriptor) { } } + +TEST(Subnet6Test, addVendorOptions) { + + uint32_t vendor_id1 = 12345678; + uint32_t vendor_id2 = 87654321; + uint32_t vendor_id_bogus = 1111111; + + // Create as subnet to add options to it. + Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4)); + + // Differentiate options by their codes (100-109) + for (uint16_t code = 100; code < 110; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(subnet->addVendorOption(option, false, vendor_id1)); + } + + // Add 7 options to another option space. The option codes partially overlap + // with option codes that we have added to dhcp6 option space. + for (uint16_t code = 105; code < 112; ++code) { + OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF))); + ASSERT_NO_THROW(subnet->addVendorOption(option, false, vendor_id2)); + } + + // Get options from the Subnet and check if all 10 are there. + Subnet::OptionContainerPtr options = subnet->getVendorOptionDescriptors(vendor_id1); + ASSERT_TRUE(options); + ASSERT_EQ(10, options->size()); + + // Validate codes of options added to dhcp6 option space. + uint16_t expected_code = 100; + for (Subnet::OptionContainer::const_iterator option_desc = options->begin(); + option_desc != options->end(); ++option_desc) { + ASSERT_TRUE(option_desc->option); + EXPECT_EQ(expected_code, option_desc->option->getType()); + ++expected_code; + } + + options = subnet->getVendorOptionDescriptors(vendor_id2); + ASSERT_TRUE(options); + ASSERT_EQ(7, options->size()); + + // Validate codes of options added to isc option space. + expected_code = 105; + for (Subnet::OptionContainer::const_iterator option_desc = options->begin(); + option_desc != options->end(); ++option_desc) { + ASSERT_TRUE(option_desc->option); + EXPECT_EQ(expected_code, option_desc->option->getType()); + ++expected_code; + } + + // Try to get options from a non-existing option space. + options = subnet->getVendorOptionDescriptors(vendor_id_bogus); + ASSERT_TRUE(options); + EXPECT_TRUE(options->empty()); + + // Delete options from all spaces. + subnet->delVendorOptions(); + + // Make sure that all options have been removed. + options = subnet->getVendorOptionDescriptors(vendor_id1); + ASSERT_TRUE(options); + EXPECT_TRUE(options->empty()); + + options = subnet->getVendorOptionDescriptors(vendor_id2); + ASSERT_TRUE(options); + EXPECT_TRUE(options->empty()); +} + + + // This test verifies that inRange() and inPool() methods work properly. TEST(Subnet6Test, inRangeinPool) { Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));