Parcourir la source

[4097a] Finished the DHCPv6 part

Francis Dupont il y a 9 ans
Parent
commit
c50a482e47

+ 100 - 46
src/bin/dhcp4/tests/config_parser_unittest.cc

@@ -1826,10 +1826,8 @@ TEST_F(Dhcp4ParserTest, optionStandardDefOverride) {
 
 }
 
-// Goal of this test is to verify that global option
-// data is configured for the subnet if the subnet
-// configuration does not include options configuration.
-TEST_F(Dhcp4ParserTest, optionDataDefaults) {
+// Goal of this test is to verify that global option data is configured
+TEST_F(Dhcp4ParserTest, optionDataDefaultsGlobal) {
     ConstElementPtr x;
     string config = "{ " + genIfaceConfig() + "," +
         "\"rebind-timer\": 2000,"
@@ -1855,10 +1853,78 @@ TEST_F(Dhcp4ParserTest, optionDataDefaults) {
     EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
     checkResult(x, 0);
 
+    // These options are global
     Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
         getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
     ASSERT_TRUE(subnet);
     OptionContainerPtr options = subnet->getCfgOption()->getAll("dhcp4");
+    ASSERT_EQ(0, options->size());
+
+    options = CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp4");
+    ASSERT_EQ(2, options->size());
+
+    // Get the search index. Index #1 is to search using option code.
+    const OptionContainerTypeIndex& idx = options->get<1>();
+
+    // Get the options for specified index. Expecting one option to be
+    // returned but in theory we may have multiple options with the same
+    // code so we get the range.
+    std::pair<OptionContainerTypeIndex::const_iterator,
+              OptionContainerTypeIndex::const_iterator> range =
+        idx.equal_range(56);
+    // Expect single option with the code equal to 56.
+    ASSERT_EQ(1, std::distance(range.first, range.second));
+    const uint8_t foo_expected[] = {
+        0xAB, 0xCD, 0xEF, 0x01, 0x05
+    };
+    // Check if option is valid in terms of code and carried data.
+    testOption(*range.first, 56, foo_expected, sizeof(foo_expected));
+
+    range = idx.equal_range(23);
+    ASSERT_EQ(1, std::distance(range.first, range.second));
+    // Do another round of testing with second option.
+    const uint8_t foo2_expected[] = {
+        0x01
+    };
+    testOption(*range.first, 23, foo2_expected, sizeof(foo2_expected));
+}
+
+// Goal of this test is to verify that subnet option data is configured
+TEST_F(Dhcp4ParserTest, optionDataDefaultsSubnet) {
+    ConstElementPtr x;
+    string config = "{ " + genIfaceConfig() + "," +
+        "\"rebind-timer\": 2000,"
+        "\"renew-timer\": 1000,"
+        "\"subnet4\": [ { "
+        "    \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ],"
+        "    \"subnet\": \"192.0.2.0/24\","
+        "    \"option-data\": [ {"
+        "        \"name\": \"dhcp-message\","
+        "        \"data\": \"ABCDEF0105\","
+        "        \"csv-format\": False"
+        "     },"
+        "     {"
+        "        \"name\": \"default-ip-ttl\","
+        "        \"data\": \"01\","
+        "        \"csv-format\": False"
+        "     } ]"
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+    checkResult(x, 0);
+
+    // These options are subnet options
+    OptionContainerPtr options =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp4");
+    ASSERT_EQ(0, options->size());
+
+    Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
+        getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
+    ASSERT_TRUE(subnet);
+    options = subnet->getCfgOption()->getAll("dhcp4");
     ASSERT_EQ(2, options->size());
 
     // Get the search index. Index #1 is to search using option code.
@@ -1933,21 +1999,21 @@ TEST_F(Dhcp4ParserTest, optionDataTwoSpaces) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Options should be now available for the subnet.
-    Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
-        getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.200"));
-    ASSERT_TRUE(subnet);
+    // Options should be now available
     // Try to get the option from the space dhcp4.
-    OptionDescriptor desc1 = subnet->getCfgOption()->get("dhcp4", 56);
+    OptionDescriptor desc1 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("dhcp4", 56);
     ASSERT_TRUE(desc1.option_);
     EXPECT_EQ(56, desc1.option_->getType());
     // Try to get the option from the space isc.
-    OptionDescriptor desc2 = subnet->getCfgOption()->get("isc", 56);
+    OptionDescriptor desc2 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("isc", 56);
     ASSERT_TRUE(desc2.option_);
     EXPECT_EQ(56, desc1.option_->getType());
     // Try to get the non-existing option from the non-existing
     // option space and  expect that option is not returned.
-    OptionDescriptor desc3 = subnet->getCfgOption()->get("non-existing", 56);
+    OptionDescriptor desc3 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("non-existing", 56);
     ASSERT_FALSE(desc3.option_);
 }
 
@@ -1960,8 +2026,8 @@ TEST_F(Dhcp4ParserTest, optionDataTwoSpaces) {
 TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
 
     // @todo DHCP configurations has many dependencies between
-    // parameters. First of all, configuration for subnet is
-    // inherited from the global values. Thus subnet has to be
+    // parameters. First of all, configuration for subnet was
+    // inherited from the global values. Thus subnet had to be
     // configured when all global values have been configured.
     // Also, an option can encapsulate another option only
     // if the latter has been configured. For this reason in this
@@ -2011,7 +2077,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
     CfgMgr::instance().clear();
 
     // Stage 2. Configure base option and a subnet. Please note that
-    // the configuration from the stage 2 is repeated because BIND
+    // the configuration from the stage 2 is repeated because Kea
     // configuration manager sends whole configuration for the lists
     // where at least one element is being modified or added.
     config = "{ " + genIfaceConfig() + "," +
@@ -2063,18 +2129,15 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Get the subnet.
-    Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
-        getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.5"));
-    ASSERT_TRUE(subnet);
-
     // We should have one option available.
-    OptionContainerPtr options = subnet->getCfgOption()->getAll("dhcp4");
+    OptionContainerPtr options =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp4");
     ASSERT_TRUE(options);
     ASSERT_EQ(1, options->size());
 
     // Get the option.
-    OptionDescriptor desc = subnet->getCfgOption()->get("dhcp4", 222);
+    OptionDescriptor desc =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("dhcp4", 222);
     EXPECT_TRUE(desc.option_);
     EXPECT_EQ(222, desc.option_->getType());
 
@@ -2605,19 +2668,15 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Get the subnet.
-    Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
-        getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.5"));
-    ASSERT_TRUE(subnet);
-
     // We should have one option available.
-    OptionContainerPtr options = subnet->getCfgOption()->getAll("dhcp4");
+    OptionContainerPtr options =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp4");
     ASSERT_TRUE(options);
     ASSERT_EQ(1, options->size());
 
     // Get the option.
-    OptionDescriptor desc =
-        subnet->getCfgOption()->get("dhcp4", DHO_VENDOR_ENCAPSULATED_OPTIONS);
+    OptionDescriptor desc = CfgMgr::instance().getStagingCfg()->
+        getCfgOption()->get("dhcp4", DHO_VENDOR_ENCAPSULATED_OPTIONS);
     EXPECT_TRUE(desc.option_);
     EXPECT_EQ(DHO_VENDOR_ENCAPSULATED_OPTIONS, desc.option_->getType());
 
@@ -2651,7 +2710,7 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
 }
 
 // This test checks if vendor options can be specified in the config file
-// (in hex format), and later retrieved from configured subnet
+// (in hex format), and later retrieved
 TEST_F(Dhcp4ParserTest, vendorOptionsHex) {
 
     // This configuration string is to configure two options
@@ -2689,28 +2748,26 @@ TEST_F(Dhcp4ParserTest, vendorOptionsHex) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Options should be now available for the subnet.
-    Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
-        getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.5"));
-    ASSERT_TRUE(subnet);
-
     // Try to get the option from the vendor space 4491
-    OptionDescriptor desc1 = subnet->getCfgOption()->get(VENDOR_ID_CABLE_LABS, 100);
+    OptionDescriptor desc1 = CfgMgr::instance().getStagingCfg()->
+        getCfgOption()->get(VENDOR_ID_CABLE_LABS, 100);
     ASSERT_TRUE(desc1.option_);
     EXPECT_EQ(100, desc1.option_->getType());
     // Try to get the option from the vendor space 1234
-    OptionDescriptor desc2 = subnet->getCfgOption()->get(1234, 100);
+    OptionDescriptor desc2 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(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.
-    OptionDescriptor desc3 = subnet->getCfgOption()->get(5678, 100);
+    OptionDescriptor desc3 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(5678, 100);
     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
+// (in csv format), and later retrieved
 TEST_F(Dhcp4ParserTest, vendorOptionsCsv) {
 
     // This configuration string is to configure two options
@@ -2746,19 +2803,16 @@ TEST_F(Dhcp4ParserTest, vendorOptionsCsv) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Options should be now available for the subnet.
-    Subnet4Ptr subnet = CfgMgr::instance().getStagingCfg()->
-        getCfgSubnets4()->selectSubnet(IOAddress("192.0.2.5"));
-    ASSERT_TRUE(subnet);
-
     // Try to get the option from the vendor space 4491
-    OptionDescriptor desc1 = subnet->getCfgOption()->get(VENDOR_ID_CABLE_LABS, 100);
+    OptionDescriptor desc1 = CfgMgr::instance().getStagingCfg()->
+        getCfgOption()->get(VENDOR_ID_CABLE_LABS, 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.
-    OptionDescriptor desc2 = subnet->getCfgOption()->get(5678, 100);
+    OptionDescriptor desc2 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(5678, 100);
     ASSERT_FALSE(desc2.option_);
 }
 

+ 6 - 4
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -1844,8 +1844,10 @@ TEST_F(Dhcpv4SrvTest, subnetClassPriority) {
 }
 
 // Checks class options have the priority over global options
-// Note it is not currently the case, cf #4205
 TEST_F(Dhcpv4SrvTest, classGlobalPriority) {
+    IfaceMgrTestConfig test_config(true);
+    IfaceMgr::instance().openSockets4();
+
     NakedDhcpv4Srv srv(0);
 
     // A global ip-forwarding option is set in the response.
@@ -1914,13 +1916,13 @@ TEST_F(Dhcpv4SrvTest, classGlobalPriority) {
     ASSERT_TRUE(opt);
     ASSERT_GT(opt->len(), opt->getHeaderLen());
     // Classification sets the value to true/1, global to false/0
-    // Here class should have the priority but hasn't, cf #4205
-    EXPECT_EQ(0, opt->getUint8());
+    // Here class has the priority
+    EXPECT_NE(0, opt->getUint8());
 }
 
 // Checks if the client-class field is indeed used for subnet selection.
 // Note that packet classification is already checked in Dhcpv4SrvTest
-// .*clientClassification above.
+// .*Classification above.
 TEST_F(Dhcpv4SrvTest, clientClassify) {
 
     // This test configures 2 subnets. We actually only need the

+ 44 - 31
src/bin/dhcp6/dhcp6_srv.cc

@@ -884,16 +884,16 @@ Dhcpv6Srv::copyClientOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
 }
 
 void
-Dhcpv6Srv::appendDefaultOptions(const Pkt6Ptr&, Pkt6Ptr& answer) {
+Dhcpv6Srv::appendDefaultOptions(const Pkt6Ptr&, Pkt6Ptr& answer,
+                                const CfgOptionList&) {
     // add server-id
     answer->addOption(getServerID());
 }
 
 void
 Dhcpv6Srv::buildCfgOptionList(const Pkt6Ptr& question,
-                              AllocEngine::ClientContext6& ctx) {
-    CfgOptionList& co_list = getCfgOptionList();
-
+                              AllocEngine::ClientContext6& ctx,
+                              CfgOptionList& co_list) {
     // First subnet configured options
     if (ctx.subnet_) {
         co_list.push_back(ctx.subnet_->getCfgOption());
@@ -921,7 +921,8 @@ Dhcpv6Srv::buildCfgOptionList(const Pkt6Ptr& question,
 
 void
 Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
-                                  AllocEngine::ClientContext6& ctx) {
+                                  AllocEngine::ClientContext6& ctx,
+                                  const CfgOptionList& co_list) {
 
     // Client requests some options using ORO option. Try to
     // get this option from client's message.
@@ -938,7 +939,6 @@ Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
     const std::vector<uint16_t>& requested_opts = option_oro->getValues();
     BOOST_FOREACH(uint16_t opt, requested_opts) {
         // Iterate on the configured option list
-        const CfgOptionList& co_list = getCfgOptionList();
         for (CfgOptionList::const_iterator copts = co_list.begin();
              copts != co_list.end(); ++copts) {
             OptionDescriptor desc = (*copts)->get("dhcp6", opt);
@@ -952,8 +952,10 @@ Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
 }
 
 void
-Dhcpv6Srv::appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
-                                        AllocEngine::ClientContext6& ctx) {
+Dhcpv6Srv::appendRequestedVendorOptions(const Pkt6Ptr& question,
+                                        Pkt6Ptr& answer,
+                                        AllocEngine::ClientContext6& ctx,
+                                        const CfgOptionList& co_list) {
 
     // Leave if there is no subnet matching the incoming packet.
     // There is no need to log the error message here because
@@ -990,7 +992,6 @@ Dhcpv6Srv::appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer
     bool added = false;
     const std::vector<uint16_t>& requested_opts = oro->getValues();
     BOOST_FOREACH(uint16_t opt, requested_opts) {
-        const CfgOptionList& co_list = getCfgOptionList();
         for (CfgOptionList::const_iterator copts = co_list.begin();
              copts != co_list.end(); ++copts) {
             OptionDescriptor desc = (*copts)->get(vendor_id, opt);
@@ -2320,10 +2321,11 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     }
 
     copyClientOptions(solicit, response);
-    buildCfgOptionList(solicit, ctx);
-    appendDefaultOptions(solicit, response);
-    appendRequestedOptions(solicit, response, ctx);
-    appendRequestedVendorOptions(solicit, response, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(solicit, ctx, co_list);
+    appendDefaultOptions(solicit, response, co_list);
+    appendRequestedOptions(solicit, response, ctx, co_list);
+    appendRequestedVendorOptions(solicit, response, ctx, co_list);
 
     processClientFqdn(solicit, response, ctx);
     assignLeases(solicit, response, ctx);
@@ -2348,10 +2350,11 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, request->getTransid()));
 
     copyClientOptions(request, reply);
-    buildCfgOptionList(request, ctx);
-    appendDefaultOptions(request, reply);
-    appendRequestedOptions(request, reply, ctx);
-    appendRequestedVendorOptions(request, reply, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(request, ctx, co_list);
+    appendDefaultOptions(request, reply, co_list);
+    appendRequestedOptions(request, reply, ctx, co_list);
+    appendRequestedVendorOptions(request, reply, ctx, co_list);
 
     processClientFqdn(request, reply, ctx);
     assignLeases(request, reply, ctx);
@@ -2372,9 +2375,10 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, renew->getTransid()));
 
     copyClientOptions(renew, reply);
-    buildCfgOptionList(renew, ctx);
-    appendDefaultOptions(renew, reply);
-    appendRequestedOptions(renew, reply, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(renew, ctx, co_list);
+    appendDefaultOptions(renew, reply, co_list);
+    appendRequestedOptions(renew, reply, ctx, co_list);
 
     processClientFqdn(renew, reply, ctx);
     extendLeases(renew, reply, ctx);
@@ -2395,9 +2399,10 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, rebind->getTransid()));
 
     copyClientOptions(rebind, reply);
-    buildCfgOptionList(rebind, ctx);
-    appendDefaultOptions(rebind, reply);
-    appendRequestedOptions(rebind, reply, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(rebind, ctx, co_list);
+    appendDefaultOptions(rebind, reply, co_list);
+    appendRequestedOptions(rebind, reply, ctx, co_list);
 
     processClientFqdn(rebind, reply, ctx);
     extendLeases(rebind, reply, ctx);
@@ -2426,9 +2431,10 @@ Dhcpv6Srv::processConfirm(const Pkt6Ptr& confirm) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, confirm->getTransid()));
     // Make sure that the necessary options are included.
     copyClientOptions(confirm, reply);
-    buildCfgOptionList(confirm, ctx);
-    appendDefaultOptions(confirm, reply);
-    appendRequestedOptions(confirm, reply, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(confirm, ctx, co_list);
+    appendDefaultOptions(confirm, reply, co_list);
+    appendRequestedOptions(confirm, reply, ctx, co_list);
     // Indicates if at least one address has been verified. If no addresses
     // are verified it means that the client has sent no IA_NA options
     // or no IAAddr options and that client's message has to be discarded.
@@ -2507,7 +2513,9 @@ Dhcpv6Srv::processRelease(const Pkt6Ptr& release) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, release->getTransid()));
 
     copyClientOptions(release, reply);
-    appendDefaultOptions(release, reply);
+    CfgOptionList co_list;
+    // buildCfgOptionList(release, ctx, co_list);
+    appendDefaultOptions(release, reply, co_list);
 
     releaseLeases(release, reply, ctx);
 
@@ -2532,8 +2540,12 @@ Dhcpv6Srv::processDecline(const Pkt6Ptr& decline) {
     // Copy client options (client-id, also relay information if present)
     copyClientOptions(decline, reply);
 
+    // Get the configured option list
+    CfgOptionList co_list;
+    buildCfgOptionList(decline, ctx, co_list);
+
     // Include server-id
-    appendDefaultOptions(decline, reply);
+    appendDefaultOptions(decline, reply, co_list);
 
     if (declineLeases(decline, reply, ctx)) {
         return (reply);
@@ -2808,15 +2820,16 @@ Dhcpv6Srv::processInfRequest(const Pkt6Ptr& inf_request) {
     copyClientOptions(inf_request, reply);
 
     // Build the configured option list for append methods
-    buildCfgOptionList(inf_request, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(inf_request, ctx, co_list);
 
     // Append default options, i.e. options that the server is supposed
     // to put in all messages it sends (server-id for now, but possibly other
     // options once we start supporting authentication)
-    appendDefaultOptions(inf_request, reply);
+    appendDefaultOptions(inf_request, reply, co_list);
 
     // Try to assign options that were requested by the client.
-    appendRequestedOptions(inf_request, reply, ctx);
+    appendRequestedOptions(inf_request, reply, ctx, co_list);
 
     return (reply);
 }

+ 14 - 18
src/bin/dhcp6/dhcp6_srv.h

@@ -429,24 +429,17 @@ protected:
     /// @param answer server's message (options will be copied here)
     void copyClientOptions(const Pkt6Ptr& question, Pkt6Ptr& answer);
 
-    /// @brief Returns the configured option list
-    CfgOptionList& getCfgOptionList() {
-        return (cfg_option_list_);
-    }
-
-    /// @brief Returns the configured option list
-    const CfgOptionList& getCfgOptionList() const {
-        return (cfg_option_list_);
-    }
-
     /// @brief Build the configured option list
     ///
     /// @note The configured option list is an *ordered* list of
     /// @c CfgOption objects used to append options to the response.
     ///
-    /// @param ex The exchange where the configured option list is cached
+    /// @param question client's message
+    /// @param ctx client context (for the subnet)
+    /// @param co_list configured option list to build
     void buildCfgOptionList(const Pkt6Ptr& question,
-                            AllocEngine::ClientContext6& ctx);
+                            AllocEngine::ClientContext6& ctx,
+                            CfgOptionList& co_list);
 
     /// @brief Appends default options to server's answer.
     ///
@@ -456,7 +449,9 @@ protected:
     ///
     /// @param question client's message
     /// @param answer server's message (options will be added here)
-    void appendDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer);
+    /// @param co_list configured option list (currently unused)
+    void appendDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
+                              const CfgOptionList& co_list);
 
     /// @brief Appends requested options to server's answer.
     ///
@@ -465,8 +460,10 @@ protected:
     /// @param question client's message
     /// @param answer server's message (options will be added here)
     /// @param ctx client context (contains subnet, duid and other parameters)
+    /// @param co_list configured option list
     void appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
-                                AllocEngine::ClientContext6& ctx);
+                                AllocEngine::ClientContext6& ctx,
+                                const CfgOptionList& co_list);
 
     /// @brief Appends requested vendor options to server's answer.
     ///
@@ -476,8 +473,10 @@ protected:
     /// @param question client's message
     /// @param answer server's message (vendor options will be added here)
     /// @param ctx client context (contains subnet, duid and other parameters)
+    /// @param co_list configured option list
     void appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
-                                AllocEngine::ClientContext6& ctx);
+                                      AllocEngine::ClientContext6& ctx,
+                                      const CfgOptionList& co_list);
 
     /// @brief Assigns leases.
     ///
@@ -825,9 +824,6 @@ private:
     /// UDP port number on which server listens.
     uint16_t port_;
 
-    /// @brief Configured option list for appending otions.
-    CfgOptionList cfg_option_list_;
-
 protected:
 
     /// Indicates if shutdown is in progress. Setting it to true will

+ 101 - 33
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -2057,10 +2057,8 @@ TEST_F(Dhcp6ParserTest, optionStandardDefOverride) {
     EXPECT_FALSE(def->getArrayType());
 }
 
-// Goal of this test is to verify that global option
-// data is configured for the subnet if the subnet
-// configuration does not include options configuration.
-TEST_F(Dhcp6ParserTest, optionDataDefaults) {
+// Goal of this test is to verify that global option data is configured
+TEST_F(Dhcp6ParserTest, optionDataDefaultsGlobal) {
     ConstElementPtr x;
     string config = "{ " + genIfaceConfig() + ","
         "\"preferred-lifetime\": 3000,"
@@ -2086,10 +2084,86 @@ TEST_F(Dhcp6ParserTest, optionDataDefaults) {
     EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
     checkResult(x, 0);
 
+    // These options are global
     Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
         selectSubnet(IOAddress("2001:db8:1::5"), classify_);
     ASSERT_TRUE(subnet);
     OptionContainerPtr options = subnet->getCfgOption()->getAll("dhcp6");
+    ASSERT_EQ(0, options->size());
+
+    options = CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp6");
+    ASSERT_EQ(2, options->size());
+
+    // Get the search index. Index #1 is to search using option code.
+    const OptionContainerTypeIndex& idx = options->get<1>();
+
+    // Get the options for specified index. Expecting one option to be
+    // returned but in theory we may have multiple options with the same
+    // code so we get the range.
+    std::pair<OptionContainerTypeIndex::const_iterator,
+              OptionContainerTypeIndex::const_iterator> range =
+        idx.equal_range(D6O_SUBSCRIBER_ID);
+    // Expect single option with the code equal to 38.
+    ASSERT_EQ(1, std::distance(range.first, range.second));
+    const uint8_t subid_expected[] = {
+        0xAB, 0xCD, 0xEF, 0x01, 0x05
+    };
+    // Check if option is valid in terms of code and carried data.
+    testOption(*range.first, D6O_SUBSCRIBER_ID, subid_expected,
+               sizeof(subid_expected));
+
+    range = idx.equal_range(D6O_PREFERENCE);
+    ASSERT_EQ(1, std::distance(range.first, range.second));
+    // Do another round of testing with second option.
+    const uint8_t pref_expected[] = {
+        0x01
+    };
+    testOption(*range.first, D6O_PREFERENCE, pref_expected,
+               sizeof(pref_expected));
+
+    // Check that options with other option codes are not returned.
+    for (uint16_t code = 47; code < 57; ++code) {
+        range = idx.equal_range(code);
+        EXPECT_EQ(0, std::distance(range.first, range.second));
+    }
+}
+
+// Goal of this test is to verify that subnet option data is configured
+TEST_F(Dhcp6ParserTest, optionDataDefaultsSubnet) {
+    ConstElementPtr x;
+    string config = "{ " + genIfaceConfig() + ","
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000,"
+        "\"renew-timer\": 1000,"
+        "\"subnet6\": [ { "
+        "    \"pools\": [ { \"pool\": \"2001:db8:1::/80\" } ],"
+        "    \"subnet\": \"2001:db8:1::/64\","
+        "    \"option-data\": [ {"
+        "        \"name\": \"subscriber-id\","
+        "        \"data\": \"ABCDEF0105\","
+        "        \"csv-format\": False"
+        "     },"
+        "     {"
+        "        \"name\": \"preference\","
+        "        \"data\": \"01\""
+        "     } ]"
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+    checkResult(x, 0);
+
+    // These options are subnet options
+    OptionContainerPtr options =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp6");
+    ASSERT_EQ(0, options->size());
+
+    Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
+        selectSubnet(IOAddress("2001:db8:1::5"), classify_);
+    ASSERT_TRUE(subnet);
+    options = subnet->getCfgOption()->getAll("dhcp6");
     ASSERT_EQ(2, options->size());
 
     // Get the search index. Index #1 is to search using option code.
@@ -2173,21 +2247,21 @@ TEST_F(Dhcp6ParserTest, optionDataTwoSpaces) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Options should be now available for the subnet.
-    Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
-        selectSubnet(IOAddress("2001:db8:1::5"), classify_);
-    ASSERT_TRUE(subnet);
+    // Options should be now available
     // Try to get the option from the space dhcp6.
-    OptionDescriptor desc1 = subnet->getCfgOption()->get("dhcp6", 38);
+    OptionDescriptor desc1 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("dhcp6", 38);
     ASSERT_TRUE(desc1.option_);
     EXPECT_EQ(38, desc1.option_->getType());
     // Try to get the option from the space isc.
-    OptionDescriptor desc2 = subnet->getCfgOption()->get("isc", 38);
+    OptionDescriptor desc2 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("isc", 38);
     ASSERT_TRUE(desc2.option_);
     EXPECT_EQ(38, desc1.option_->getType());
     // Try to get the non-existing option from the non-existing
     // option space and  expect that option is not returned.
-    OptionDescriptor desc3 = subnet->getCfgOption()->get("non-existing", 38);
+    OptionDescriptor desc3 = CfgMgr::instance().getStagingCfg()->
+        getCfgOption()->get("non-existing", 38);
     ASSERT_FALSE(desc3.option_);
 }
 
@@ -2306,18 +2380,15 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Get the subnet.
-    Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
-        selectSubnet(IOAddress("2001:db8:1::5"), classify_);
-    ASSERT_TRUE(subnet);
-
     // We should have one option available.
-    OptionContainerPtr options = subnet->getCfgOption()->getAll("dhcp6");
+    OptionContainerPtr options =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->getAll("dhcp6");
     ASSERT_TRUE(options);
     ASSERT_EQ(1, options->size());
 
     // Get the option.
-    OptionDescriptor desc = subnet->getCfgOption()->get("dhcp6", 100);
+    OptionDescriptor desc =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get("dhcp6", 100);
     EXPECT_TRUE(desc.option_);
     EXPECT_EQ(100, desc.option_->getType());
 
@@ -2681,23 +2752,22 @@ TEST_F(Dhcp6ParserTest, vendorOptionsHex) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Options should be now available for the subnet.
-    Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
-        selectSubnet(IOAddress("2001:db8:1::5"), classify_);
-    ASSERT_TRUE(subnet);
-
+    // Options should be now available
     // Try to get the option from the vendor space 4491
-    OptionDescriptor desc1 = subnet->getCfgOption()->get(4491, 100);
+    OptionDescriptor desc1 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(4491, 100);
     ASSERT_TRUE(desc1.option_);
     EXPECT_EQ(100, desc1.option_->getType());
     // Try to get the option from the vendor space 1234
-    OptionDescriptor desc2 = subnet->getCfgOption()->get(1234, 100);
+    OptionDescriptor desc2 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(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.
-    OptionDescriptor desc3 = subnet->getCfgOption()->get(5678, 38);
+    OptionDescriptor desc3 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(5678, 38);
     ASSERT_FALSE(desc3.option_);
 }
 
@@ -2739,19 +2809,17 @@ TEST_F(Dhcp6ParserTest, vendorOptionsCsv) {
     ASSERT_TRUE(status);
     checkResult(status, 0);
 
-    // Options should be now available for the subnet.
-    Subnet6Ptr subnet = CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->
-        selectSubnet(IOAddress("2001:db8:1::5"), classify_);
-    ASSERT_TRUE(subnet);
-
+    // Options should be now available.
     // Try to get the option from the vendor space 4491
-    OptionDescriptor desc1 = subnet->getCfgOption()->get(4491, 100);
+    OptionDescriptor desc1 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(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.
-    OptionDescriptor desc2 = subnet->getCfgOption()->get(5678, 100);
+    OptionDescriptor desc2 =
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->get(5678, 100);
     ASSERT_FALSE(desc2.option_);
 }
 

+ 247 - 5
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -1807,8 +1807,8 @@ TEST_F(Dhcpv6SrvTest, unpackOptions) {
     EXPECT_EQ(0x0, option_bar->getValue());
 }
 
-// Checks if client packets are classified properly
-TEST_F(Dhcpv6SrvTest, clientClassification) {
+// Checks if DOCSIS client packets are classified properly
+TEST_F(Dhcpv6SrvTest, docsisClientClassification) {
 
     NakedDhcpv6Srv srv(0);
 
@@ -1836,10 +1836,252 @@ TEST_F(Dhcpv6SrvTest, clientClassification) {
     EXPECT_FALSE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
 }
 
+// Checks if client packets are classified properly using match expressions.
+TEST_F(Dhcpv6SrvTest, matchClassification) {
+    IfaceMgrTestConfig test_config(true);
+
+    NakedDhcpv6Srv srv(0);
+
+    // The router class matches incoming packets with foo in a host-name
+    // option (code 1234) and sets an ipv6-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"option-def\": [ "
+        "{   \"name\": \"host-name\","
+        "    \"code\": 1234,"
+        "    \"type\": \"string\" },"
+        "{   \"name\": \"ipv6-forwarding\","
+        "    \"code\": 2345,"
+        "    \"type\": \"boolean\" }],"
+        "\"subnet6\": [ "
+        "{   \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], "
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"eth1\" } ],"
+        "\"client-classes\": [ "
+        "{   \"name\": \"router\", "
+        "    \"option-data\": ["
+        "        {    \"name\": \"ipv6-forwarding\", "
+        "             \"data\": \"true\" } ], "
+        "    \"test\": \"option[1234] == 'foo'\" } ] }";
+    ASSERT_NO_THROW(configure(config));
+
+    // Create packets with enough to select the subnet
+    OptionPtr clientid = generateClientId();
+    Pkt6Ptr query1(new Pkt6(DHCPV6_SOLICIT, 1234));
+    query1->setRemoteAddr(IOAddress("fe80::abcd"));
+    query1->addOption(clientid);
+    query1->setIface("eth1");
+    query1->addOption(generateIA(D6O_IA_NA, 123, 1500, 3000));
+    Pkt6Ptr query2(new Pkt6(DHCPDISCOVER, 1234));
+    query2->setRemoteAddr(IOAddress("fe80::abcd"));
+    query2->addOption(clientid);
+    query2->setIface("eth1");
+    query2->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+    Pkt6Ptr query3(new Pkt6(DHCPDISCOVER, 1234));
+    query3->setRemoteAddr(IOAddress("fe80::abcd"));
+    query3->addOption(clientid);
+    query3->setIface("eth1");
+    query3->addOption(generateIA(D6O_IA_NA, 345, 1500, 3000));
+
+    // Create and add an ORO option to the first 2 queries
+    OptionUint16ArrayPtr oro(new OptionUint16Array(Option::V6, D6O_ORO));
+    ASSERT_TRUE(oro);
+    oro->addValue(2345);
+    query1->addOption(oro);
+    query2->addOption(oro);
+
+    // Create and add a host-name option to the first and last queries
+    OptionStringPtr hostname(new OptionString(Option::V6, 1234, "foo"));
+    ASSERT_TRUE(hostname);
+    query1->addOption(hostname);
+    query3->addOption(hostname);
+
+    // Classify packets
+    srv.classifyPacket(query1);
+    srv.classifyPacket(query2);
+    srv.classifyPacket(query3);
+
+    // Packets at the exception of the second should be in the router class
+    EXPECT_TRUE(query1->inClass("router"));
+    EXPECT_FALSE(query2->inClass("router"));
+    EXPECT_TRUE(query3->inClass("router"));
+
+    // Process queries
+    Pkt6Ptr response1 = srv.processSolicit(query1);
+    Pkt6Ptr response2 = srv.processSolicit(query2);
+    Pkt6Ptr response3 = srv.processSolicit(query3);
+
+    // Classification processing should add an ip-forwarding option
+    OptionPtr opt1 = response1->getOption(2345);
+    EXPECT_TRUE(opt1);
+
+    // But only for the first exchange: second was not classified
+    OptionPtr opt2 = response2->getOption(2345);
+    EXPECT_FALSE(opt2);
+
+    // But only for the first exchange: third has no ORO
+    OptionPtr opt3 = response3->getOption(2345);
+    EXPECT_FALSE(opt3);
+}
+
+// Checks subnet options have the priority over class options
+TEST_F(Dhcpv6SrvTest, subnetClassPriority) {
+    IfaceMgrTestConfig test_config(true);
+
+    NakedDhcpv6Srv srv(0);
+
+    // Subnet sets an ipv6-forwarding option in the response.
+    // The router class matches incoming packets with foo in a host-name
+    // option (code 1234) and sets an ipv6-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"option-def\": [ "
+        "{   \"name\": \"host-name\","
+        "    \"code\": 1234,"
+        "    \"type\": \"string\" },"
+        "{   \"name\": \"ipv6-forwarding\","
+        "    \"code\": 2345,"
+        "    \"type\": \"boolean\" }],"
+        "\"subnet6\": [ "
+        "{   \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], "
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"eth1\", "
+        "    \"option-data\": ["
+        "        {    \"name\": \"ipv6-forwarding\", "
+        "             \"data\": \"false\" } ] } ], "
+        "\"client-classes\": [ "
+        "{   \"name\": \"router\","
+        "    \"option-data\": ["
+        "        {    \"name\": \"ipv6-forwarding\", "
+        "             \"data\": \"true\" } ], "
+        "    \"test\": \"option[1234] == 'foo'\" } ] }";
+    ASSERT_NO_THROW(configure(config));
+
+    // Create a packet with enough to select the subnet and go through
+    // the SOLICIT processing
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    query->setRemoteAddr(IOAddress("fe80::abcd"));
+    OptionPtr clientid = generateClientId();
+    query->addOption(clientid);
+    query->setIface("eth1");
+    query->addOption(generateIA(D6O_IA_NA, 123, 1500, 3000));
+
+    // Create and add a ORO option to the query
+    OptionUint16ArrayPtr oro(new OptionUint16Array(Option::V6, D6O_ORO));
+    ASSERT_TRUE(oro);
+    oro->addValue(2345);
+    query->addOption(oro);
+
+    // Create and add a host-name option to the query
+    OptionStringPtr hostname(new OptionString(Option::V6, 1234, "foo"));
+    ASSERT_TRUE(hostname);
+    query->addOption(hostname);
+
+    // Classify the packet
+    srv.classifyPacket(query);
+
+    // The packet should be in the router class
+    EXPECT_TRUE(query->inClass("router"));
+
+    // Process the query
+    Pkt6Ptr response = srv.processSolicit(query);
+
+    // A processing should add an ip-forwarding option
+    OptionPtr opt = response->getOption(2345);
+    ASSERT_TRUE(opt);
+    ASSERT_GT(opt->len(), opt->getHeaderLen());
+    // Classification sets the value to true/1, subnet to false/0
+    // Here subnet has the priority
+    EXPECT_EQ(0, opt->getUint8());
+}
+
+// Checks class options have the priority over global options
+TEST_F(Dhcpv6SrvTest, classGlobalPriority) {
+    IfaceMgrTestConfig test_config(true);
+
+    NakedDhcpv6Srv srv(0);
+
+    // A global ipv6-forwarding option is set in the response.
+    // The router class matches incoming packets with foo in a host-name
+    // option (code 1234) and sets an ipv6-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"option-def\": [ "
+        "{   \"name\": \"host-name\","
+        "    \"code\": 1234,"
+        "    \"type\": \"string\" },"
+        "{   \"name\": \"ipv6-forwarding\","
+        "    \"code\": 2345,"
+        "    \"type\": \"boolean\" }],"
+        "\"subnet6\": [ "
+        "{   \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], "
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"eth1\" } ],"
+        "\"option-data\": ["
+        "    {    \"name\": \"ipv6-forwarding\", "
+        "         \"data\": \"false\" } ], "
+        "\"client-classes\": [ "
+        "{   \"name\": \"router\","
+        "    \"option-data\": ["
+        "        {    \"name\": \"ipv6-forwarding\", "
+        "             \"data\": \"true\" } ], "
+        "    \"test\": \"option[1234] == 'foo'\" } ] }";
+    ASSERT_NO_THROW(configure(config));
+
+    // Create a packet with enough to select the subnet and go through
+    // the SOLICIT processing
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    query->setRemoteAddr(IOAddress("fe80::abcd"));
+    OptionPtr clientid = generateClientId();
+    query->addOption(clientid);
+    query->setIface("eth1");
+    query->addOption(generateIA(D6O_IA_NA, 123, 1500, 3000));
+
+    // Create and add a ORO option to the query
+    OptionUint16ArrayPtr oro(new OptionUint16Array(Option::V6, D6O_ORO));
+    ASSERT_TRUE(oro);
+    oro->addValue(2345);
+    query->addOption(oro);
+
+    // Create and add a host-name option to the query
+    OptionStringPtr hostname(new OptionString(Option::V6, 1234, "foo"));
+    ASSERT_TRUE(hostname);
+    query->addOption(hostname);
+
+    // Classify the packet
+    srv.classifyPacket(query);
+
+    // The packet should be in the router class
+    EXPECT_TRUE(query->inClass("router"));
+
+    // Process the query
+    Pkt6Ptr response = srv.processSolicit(query);
+
+    // A processing should add an ip-forwarding option
+    OptionPtr opt = response->getOption(2345);
+    ASSERT_TRUE(opt);
+    ASSERT_GT(opt->len(), opt->getHeaderLen());
+    // Classification sets the value to true/1, global to false/0
+    // Here class has the priority
+    EXPECT_NE(0, opt->getUint8());
+}
+
 // Checks if the client-class field is indeed used for subnet selection.
 // Note that packet classification is already checked in Dhcpv6SrvTest
-// .clientClassification above.
-TEST_F(Dhcpv6SrvTest, clientClassify2) {
+// .*Classification above.
+TEST_F(Dhcpv6SrvTest, clientClassifySubnet) {
 
     // This test configures 2 subnets. We actually only need the
     // first one, but since there's still this ugly hack that picks
@@ -1895,7 +2137,7 @@ TEST_F(Dhcpv6SrvTest, clientClassify2) {
 
 // Tests whether a packet with custom vendor-class (not erouter or docsis)
 // is classified properly.
-TEST_F(Dhcpv6SrvTest, clientClassification3) {
+TEST_F(Dhcpv6SrvTest, vendorClientClassification2) {
     NakedDhcpv6Srv srv(0);
 
     // Let's create a SOLICIT.

+ 7 - 3
src/lib/dhcpsrv/parsers/dhcp_parsers.cc

@@ -730,6 +730,12 @@ OptionDataListParser::commit() {
     BOOST_FOREACH(ParserPtr parser, parsers_) {
         parser->commit();
     }
+    // Append suboptions to the top-level options
+    if (cfg_) {
+        cfg_->encapsulate();
+    } else {
+        CfgMgr::instance().getStagingCfg()->getCfgOption()->encapsulate();
+    }
 }
 
 // ******************************** OptionDefParser ****************************
@@ -1260,10 +1266,8 @@ SubnetConfigParser::createSubnet() {
     // options but it is no longer the case (they have a different
     // and not consecutive priority).
 
-    // Copy all options to the subnet configuration.
+    // Copy options to the subnet configuration.
     options_->copyTo(*subnet_->getCfgOption());
-    // Append suboptions to the top-level options.
-    subnet_->getCfgOption()->encapsulate();
 }
 
 isc::dhcp::Triplet<uint32_t>

+ 2 - 1
src/lib/dhcpsrv/parsers/dhcp_parsers.h

@@ -718,7 +718,8 @@ public:
 
     /// @brief Commit all option values.
     ///
-    /// This function invokes commit for all option values.
+    /// This function invokes commit for all option values
+    /// and append suboptions to the top-level options.
     void commit();
 
 private: