Commit 0764a74e authored by Marcin Siodelski's avatar Marcin Siodelski

[3194] Implemented basic vendor options support.

parent 78cb0f4f
......@@ -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);
}
};
......
......@@ -20,7 +20,9 @@
#include <dhcp/option4_addrlst.h>
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
#include <dhcp/option_vendor.h>
#include <dhcp/pkt4.h>
#include <dhcp/docsis3_option_defs.h>
#include <dhcp4/dhcp4_log.h>
#include <dhcp4/dhcp4_srv.h>
#include <dhcpsrv/addr_utilities.h>
......@@ -36,6 +38,7 @@
#include <boost/algorithm/string/erase.hpp>
#include <boost/bind.hpp>
#include <boost/foreach.hpp>
#include <iomanip>
#include <fstream>
......@@ -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<OptionVendor> vendor_req =
boost::dynamic_pointer_cast<OptionVendor>(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<OptionVendor> 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
......
......@@ -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
......
......@@ -25,8 +25,10 @@
#include <dhcp/option4_addrlst.h>
#include <dhcp/option_custom.h>
#include <dhcp/option_int_array.h>
#include <dhcp/option_vendor.h>
#include <dhcp/pkt_filter.h>
#include <dhcp/pkt_filter_inet.h>
#include <dhcp/docsis3_option_defs.h>
#include <dhcp4/dhcp4_srv.h>
#include <dhcp4/dhcp4_log.h>
#include <dhcp4/config_parser.h>
......@@ -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<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());
}
/// @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
......
......@@ -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
......
......@@ -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;
}
};
......
......@@ -17,6 +17,7 @@
#include <asiolink/io_address.h>
#include <dhcp_ddns/ncr_msg.h>
#include <dhcp/dhcp6.h>
#include <dhcp/docsis3_option_defs.h>
#include <dhcp/duid.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/libdhcp++.h>
......@@ -26,6 +27,7 @@
#include <dhcp/option6_iaaddr.h>
#include <dhcp/option6_iaprefix.h>
#include <dhcp/option_custom.h>
#include <dhcp/option_vendor.h>
#include <dhcp/option_int_array.h>
#include <dhcp/pkt6.h>
#include <dhcp6/dhcp6_log.h>
......@@ -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<OptionVendor> vendor_req =
boost::dynamic_pointer_cast<OptionVendor>(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<OptionUint16Array> oro =
boost::dynamic_pointer_cast<OptionUint16Array>(vendor_req->getOption(DOCSIS3_V6_ORO));
// Option ORO not found. Don't do anything then.
if (!oro) {
return;
}
boost::shared_ptr<OptionVendor> vendor_rsp(new OptionVendor(Option::V6, vendor_id));
// Get the list of options that client requested.
bool added = false;
const std::vector<uint16_t>& 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
......
......@@ -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
......
......@@ -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) {
......
......@@ -25,10 +25,13 @@
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
#include <dhcp/option_int.h>
#include <dhcp/option_vendor.h>
#include <dhcp/option_int_array.h>
#include <dhcp/option_string.h>
#include <dhcp/iface_mgr.h>
#include <dhcp6/config_parser.h>
#include <dhcp/dhcp6.h>
#include <dhcp/docsis3_option_defs.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/lease_mgr_factory.h>
......@@ -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<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(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<