Parcourir la source

[master] Finished merge trac4097a (Classify and append options)

Francis Dupont il y a 9 ans
Parent
commit
752ad11ff4

+ 8 - 2
ChangeLog

@@ -1,3 +1,9 @@
+1055.	[func]		[fdupont]
+	Classify match expressions are evaluated on incoming packets and
+	requested options are appended when configured by the subnet, a
+	class or globally.
+	(git #409[78], git xxx)
+
 1054.	[func]		[tmark]
 	Replaced underscores, "_", with hyphens "-", in the parameter
 	names used in the kea-ddns server's configuration as well as
@@ -5,7 +11,7 @@
 	example, "ip_address" is now "ip-address", "change_type" is
 	now "change-type".  This makes JSON element naming consistent
 	throughout Kea.
-	(Trac #4202    git 91bf527662060d4b1e294cd53e79b431edf0e910)
+	(Trac #4202, git 91bf527662060d4b1e294cd53e79b431edf0e910)
 
 1053.	[doc]		tomek
 	Support for DHCPDECLINE (v4) and DECLINE (v6) messages is
@@ -15,7 +21,7 @@
 1052.	[func]		marcin
 	libeval: expressions involving options can now use textual or
 	hexadecimal format of the options.
-	(Trac #4093 git 4cdf0fff1067b3dde6570dc6831e8b1343bc50fe)
+	(Trac #4093, git 4cdf0fff1067b3dde6570dc6831e8b1343bc50fe)
 
 1051.	[func]		[tmark]
 	kea-dhcp4 and kea-dhcp6 configuration parsing now supports

+ 1 - 0
src/bin/dhcp4/Makefile.am

@@ -75,6 +75,7 @@ kea_dhcp4_SOURCES  = main.cc
 kea_dhcp4_LDADD  = libdhcp4.la
 kea_dhcp4_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la
 kea_dhcp4_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
+kea_dhcp4_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
 kea_dhcp4_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
 kea_dhcp4_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la
 kea_dhcp4_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la

+ 6 - 0
src/bin/dhcp4/dhcp4_messages.mes

@@ -84,6 +84,12 @@ The first argument specifies the client and transaction identification
 information. The second argument includes all classes to which the
 packet has been assigned.
 
+% DHCP4_CLASS_UNCONFIGURED %1: client packet belongs to an unconfigured class: %2
+This debug message informs that incoming packet belongs to a class
+which cannot be found in the configuration. Either a hook written
+before the classification was added to Kea is used, or class naming is
+inconsistent.
+
 % DHCP4_CLIENTID_IGNORED_FOR_LEASES %1: not using client identifier for lease allocation for subnet %2
 This debug message is issued when the server is processing the DHCPv4 message
 for which client identifier will not be used when allocating new lease or

+ 162 - 30
src/bin/dhcp4/dhcp4_srv.cc

@@ -38,6 +38,8 @@
 #include <dhcpsrv/subnet_selector.h>
 #include <dhcpsrv/utils.h>
 #include <dhcpsrv/utils.h>
+#include <eval/evaluate.h>
+#include <eval/eval_messages.h>
 #include <hooks/callout_handle.h>
 #include <hooks/hooks_log.h>
 #include <hooks/hooks_manager.h>
@@ -820,6 +822,53 @@ Dhcpv4Srv::appendServerID(Dhcpv4Exchange& ex) {
 }
 
 void
+Dhcpv4Srv::buildCfgOptionList(Dhcpv4Exchange& ex) {
+    CfgOptionList& co_list = ex.getCfgOptionList();
+
+    // First subnet configured options
+    Subnet4Ptr subnet = ex.getContext()->subnet_;
+    if (!subnet) {
+        // All methods using the CfgOptionList object return soon when
+        // there is no subnet so do the same
+        return;
+    }
+    if (!subnet->getCfgOption()->empty()) {
+        co_list.push_back(subnet->getCfgOption());
+    }
+
+    // Each class in the incoming packet
+    const ClientClasses& classes = ex.getQuery()->getClasses();
+    for (ClientClasses::const_iterator cclass = classes.begin();
+         cclass != classes.end(); ++cclass) {
+        // Find the client class definition for this class
+        const ClientClassDefPtr& ccdef = CfgMgr::instance().getCurrentCfg()->
+            getClientClassDictionary()->findClass(*cclass);
+        if (!ccdef) {
+            // Not found: the class is not configured
+            if (((*cclass).size() <= VENDOR_CLASS_PREFIX.size()) ||
+                ((*cclass).compare(0, VENDOR_CLASS_PREFIX.size(), VENDOR_CLASS_PREFIX) != 0)) {
+                // Not a VENDOR_CLASS_* so should be configured
+                LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_UNCONFIGURED)
+                    .arg(ex.getQuery()->getLabel())
+                    .arg(*cclass);
+            }
+            // Skip it
+            continue;
+        }
+        if (ccdef->getCfgOption()->empty()) {
+            // Skip classes which don't configure options
+            continue;
+        }
+        co_list.push_back(ccdef->getCfgOption());
+    }
+
+    // Last global options
+    if (!CfgMgr::instance().getCurrentCfg()->getCfgOption()->empty()) {
+        co_list.push_back(CfgMgr::instance().getCurrentCfg()->getCfgOption());
+    }
+}
+
+void
 Dhcpv4Srv::appendRequestedOptions(Dhcpv4Exchange& ex) {
     // Get the subnet relevant for the client. We will need it
     // to get the options associated with it.
@@ -832,6 +881,12 @@ Dhcpv4Srv::appendRequestedOptions(Dhcpv4Exchange& ex) {
         return;
     }
 
+    // Unlikely short cut
+    const CfgOptionList& co_list = ex.getCfgOptionList();
+    if (co_list.empty()) {
+        return;
+    }
+
     Pkt4Ptr query = ex.getQuery();
 
     // try to get the 'Parameter Request List' option which holds the
@@ -852,10 +907,17 @@ Dhcpv4Srv::appendRequestedOptions(Dhcpv4Exchange& ex) {
     // to be returned to the client.
     for (std::vector<uint8_t>::const_iterator opt = requested_opts.begin();
          opt != requested_opts.end(); ++opt) {
+        // Add nothing when it is already there
         if (!resp->getOption(*opt)) {
-            OptionDescriptor desc = subnet->getCfgOption()->get("dhcp4", *opt);
-            if (desc.option_) {
-                resp->addOption(desc.option_);
+            // Iterate on the configured option list
+            for (CfgOptionList::const_iterator copts = co_list.begin();
+                 copts != co_list.end(); ++copts) {
+                OptionDescriptor desc = (*copts)->get("dhcp4", *opt);
+                // Got it: add it and jump to the outer loop
+                if (desc.option_) {
+                    resp->addOption(desc.option_);
+                    break;
+                }
             }
         }
     }
@@ -874,6 +936,12 @@ Dhcpv4Srv::appendRequestedVendorOptions(Dhcpv4Exchange& ex) {
         return;
     }
 
+    // Unlikely short cut
+    const CfgOptionList& co_list = ex.getCfgOptionList();
+    if (co_list.empty()) {
+        return;
+    }
+
     // Try to get the vendor option
     boost::shared_ptr<OptionVendor> vendor_req = boost::dynamic_pointer_cast<
         OptionVendor>(ex.getQuery()->getOption(DHO_VIVSO_SUBOPTIONS));
@@ -903,11 +971,14 @@ Dhcpv4Srv::appendRequestedVendorOptions(Dhcpv4Exchange& ex) {
     for (std::vector<uint8_t>::const_iterator code = requested_opts.begin();
          code != requested_opts.end(); ++code) {
         if  (!vendor_rsp->getOption(*code)) {
-            OptionDescriptor desc = subnet->getCfgOption()->get(vendor_id,
-                                                                *code);
-            if (desc.option_) {
-                vendor_rsp->addOption(desc.option_);
-                added = true;
+            for (CfgOptionList::const_iterator copts = co_list.begin();
+                 copts != co_list.end(); ++copts) {
+                OptionDescriptor desc = (*copts)->get(vendor_id, *code);
+                if (desc.option_) {
+                    vendor_rsp->addOption(desc.option_);
+                    added = true;
+                    break;
+                }
             }
         }
 
@@ -936,6 +1007,12 @@ Dhcpv4Srv::appendBasicOptions(Dhcpv4Exchange& ex) {
         return;
     }
 
+    // Unlikely short cut
+    const CfgOptionList& co_list = ex.getCfgOptionList();
+    if (co_list.empty()) {
+        return;
+    }
+
     Pkt4Ptr resp = ex.getResponse();
 
     // Try to find all 'required' options in the outgoing
@@ -944,10 +1021,13 @@ Dhcpv4Srv::appendBasicOptions(Dhcpv4Exchange& ex) {
         OptionPtr opt = resp->getOption(required_options[i]);
         if (!opt) {
             // Check whether option has been configured.
-            OptionDescriptor desc = subnet->getCfgOption()->
-                get("dhcp4", required_options[i]);
-            if (desc.option_) {
-                resp->addOption(desc.option_);
+            for (CfgOptionList::const_iterator copts = co_list.begin();
+                 copts != co_list.end(); ++copts) {
+                OptionDescriptor desc = (*copts)->get("dhcp4", required_options[i]);
+                if (desc.option_) {
+                    resp->addOption(desc.option_);
+                    break;
+                }
             }
         }
     }
@@ -1588,6 +1668,7 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
 
     // Adding any other options makes sense only when we got the lease.
     if (!ex.getResponse()->getYiaddr().isV4Zero()) {
+        buildCfgOptionList(ex);
         appendRequestedOptions(ex);
         appendRequestedVendorOptions(ex);
         // There are a few basic options that we always want to
@@ -1609,7 +1690,7 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
 
     /// @todo: decide whether we want to add a new hook point for
     /// doing class specific processing.
-    if (!classSpecificProcessing(ex)) {
+    if (!vendorClassSpecificProcessing(ex)) {
         /// @todo add more verbosity here
         LOG_DEBUG(options4_logger, DBG_DHCP4_DETAIL, DHCP4_DISCOVER_CLASS_PROCESSING_FAILED)
             .arg(discover->getLabel());
@@ -1643,6 +1724,7 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
 
     // Adding any other options makes sense only when we got the lease.
     if (!ex.getResponse()->getYiaddr().isV4Zero()) {
+        buildCfgOptionList(ex);
         appendRequestedOptions(ex);
         appendRequestedVendorOptions(ex);
         // There are a few basic options that we always want to
@@ -1659,7 +1741,7 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
 
     /// @todo: decide whether we want to add a new hook point for
     /// doing class specific processing.
-    if (!classSpecificProcessing(ex)) {
+    if (!vendorClassSpecificProcessing(ex)) {
         /// @todo add more verbosity here
         LOG_DEBUG(options4_logger, DBG_DHCP4_DETAIL, DHCP4_REQUEST_CLASS_PROCESSING_FAILED)
             .arg(ex.getQuery()->getLabel());
@@ -1922,6 +2004,7 @@ Dhcpv4Srv::processInform(Pkt4Ptr& inform) {
 
     Pkt4Ptr ack = ex.getResponse();
 
+    buildCfgOptionList(ex);
     appendRequestedOptions(ex);
     appendRequestedVendorOptions(ex);
     appendBasicOptions(ex);
@@ -1947,7 +2030,7 @@ Dhcpv4Srv::processInform(Pkt4Ptr& inform) {
 
     /// @todo: decide whether we want to add a new hook point for
     /// doing class specific processing.
-    if (!classSpecificProcessing(ex)) {
+    if (!vendorClassSpecificProcessing(ex)) {
         LOG_DEBUG(options4_logger, DBG_DHCP4_DETAIL,
                   DHCP4_INFORM_CLASS_PROCESSING_FAILED)
             .arg(inform->getLabel());
@@ -2241,12 +2324,11 @@ Dhcpv4Srv::unpackOptions(const OptionBuffer& buf,
     return (offset);
 }
 
-void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
+void Dhcpv4Srv::classifyByVendor(const Pkt4Ptr& pkt, std::string& classes) {
+    // Built-in vendor class processing
     boost::shared_ptr<OptionString> vendor_class =
         boost::dynamic_pointer_cast<OptionString>(pkt->getOption(DHO_VENDOR_CLASS_IDENTIFIER));
 
-    string classes = "";
-
     if (!vendor_class) {
         return;
     }
@@ -2275,19 +2357,65 @@ void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
         pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER);
         classes += string(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER) + " ";
     } else {
-        classes += VENDOR_CLASS_PREFIX + vendor_class->getValue();
         pkt->addClass(VENDOR_CLASS_PREFIX + vendor_class->getValue());
+        classes += VENDOR_CLASS_PREFIX + vendor_class->getValue();
+    }
+}
+
+void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
+    string classes = "";
+
+    // First phase: built-in vendor class processing
+    classifyByVendor(pkt, classes);
+
+    // Run match expressions
+    // Note getClientClassDictionary() cannot be null
+    const ClientClassDefMapPtr& defs_ptr = CfgMgr::instance().getCurrentCfg()->
+        getClientClassDictionary()->getClasses();
+    for (ClientClassDefMap::const_iterator it = defs_ptr->begin();
+         it != defs_ptr->end(); ++it) {
+        // Note second cannot be null
+        const ExpressionPtr& expr_ptr = it->second->getMatchExpr();
+        // Nothing to do without an expression to evaluate
+        if (!expr_ptr) {
+            continue;
+        }
+        // Evaluate the expression which can return false (no match),
+        // true (match) or raise an exception (error)
+        try {
+            bool status = evaluate(*expr_ptr, *pkt);
+            if (status) {
+                LOG_INFO(options4_logger, EVAL_RESULT)
+                    .arg(it->first)
+                    .arg(status);
+                // Matching: add the class
+                pkt->addClass(it->first);
+                classes += it->first + " ";
+            } else {
+                LOG_DEBUG(options4_logger, DBG_DHCP4_DETAIL, EVAL_RESULT)
+                    .arg(it->first)
+                    .arg(status);
+            }
+        } catch (const Exception& ex) {
+            LOG_ERROR(options4_logger, EVAL_RESULT)
+                .arg(it->first)
+                .arg(ex.what());
+        } catch (...) {
+            LOG_ERROR(options4_logger, EVAL_RESULT)
+                .arg(it->first)
+                .arg("get exception?");
+        }
     }
 
     if (!classes.empty()) {
-        LOG_DEBUG(options4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_ASSIGNED)
+        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_ASSIGNED)
             .arg(pkt->getLabel())
             .arg(classes);
     }
 }
 
 bool
-Dhcpv4Srv::classSpecificProcessing(const Dhcpv4Exchange& ex) {
+Dhcpv4Srv::vendorClassSpecificProcessing(const Dhcpv4Exchange& ex) {
 
     Subnet4Ptr subnet = ex.getContext()->subnet_;
     Pkt4Ptr query = ex.getQuery();
@@ -2306,15 +2434,19 @@ Dhcpv4Srv::classSpecificProcessing(const Dhcpv4Exchange& ex) {
 
         // Now try to set up file field in DHCPv4 packet. We will just copy
         // content of the boot-file option, which contains the same information.
-        OptionDescriptor desc = subnet->getCfgOption()->
-            get("dhcp4", DHO_BOOT_FILE_NAME);
-
-        if (desc.option_) {
-            boost::shared_ptr<OptionString> boot =
-                boost::dynamic_pointer_cast<OptionString>(desc.option_);
-            if (boot) {
-                std::string filename = boot->getValue();
-                rsp->setFile((const uint8_t*)filename.c_str(), filename.size());
+        const CfgOptionList& co_list = ex.getCfgOptionList();
+        for (CfgOptionList::const_iterator copts = co_list.begin();
+             copts != co_list.end(); ++copts) {
+            OptionDescriptor desc = (*copts)->get("dhcp4", DHO_BOOT_FILE_NAME);
+
+            if (desc.option_) {
+                boost::shared_ptr<OptionString> boot =
+                    boost::dynamic_pointer_cast<OptionString>(desc.option_);
+                if (boot) {
+                    std::string filename = boot->getValue();
+                    rsp->setFile((const uint8_t*)filename.c_str(), filename.size());
+                    break;
+                }
             }
         }
     }

+ 38 - 5
src/bin/dhcp4/dhcp4_srv.h

@@ -25,6 +25,7 @@
 #include <dhcpsrv/d2_client_mgr.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/alloc_engine.h>
+#include <dhcpsrv/cfg_option.h>
 #include <hooks/callout_handle.h>
 #include <dhcpsrv/daemon.h>
 
@@ -110,6 +111,16 @@ public:
         return (context_);
     }
 
+    /// @brief Returns the configured option list (non-const version)
+    CfgOptionList& getCfgOptionList() {
+        return (cfg_option_list_);
+    }
+
+    /// @brief Returns the configured option list (const version)
+    const CfgOptionList& getCfgOptionList() const {
+        return (cfg_option_list_);
+    }
+
 private:
 
     /// @brief Copies default parameters from client's to server's message
@@ -142,6 +153,10 @@ private:
     Pkt4Ptr resp_;
     /// @brief Context for use with allocation engine.
     AllocEngine::ClientContext4Ptr context_;
+    /// @brief Configured option list.
+    /// @note The configured option list is an *ordered* list of
+    /// @c CfgOption objects used to append options to the response.
+    CfgOptionList cfg_option_list_;
 };
 
 /// @brief Type representing the pointer to the @c Dhcpv4Exchange.
@@ -422,6 +437,14 @@ protected:
     /// @return DHCPACK to be sent to the client.
     Pkt4Ptr processInform(Pkt4Ptr& inform);
 
+    /// @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
+    void buildCfgOptionList(Dhcpv4Exchange& ex);
+
     /// @brief Appends options requested by client.
     ///
     /// This method assigns options that were requested by client
@@ -706,15 +729,17 @@ protected:
 
     /// @brief Assigns incoming packet to zero or more classes.
     ///
-    /// @note For now, the client classification is very simple. It just uses
-    /// content of the vendor-class-identifier option as a class. The resulting
-    /// class will be stored in packet (see @ref isc::dhcp::Pkt4::classes_ and
+    /// @note This is done in two phases: first the content of the
+    /// vendor-class-identifier option is used as a class, by
+    /// calling @ref classifyByVendor(). Second classification match
+    /// expressions are evaluated. The resulting classes will be stored
+    /// in the packet (see @ref isc::dhcp::Pkt4::classes_ and
     /// @ref isc::dhcp::Pkt4::inClass).
     ///
     /// @param pkt packet to be classified
     void classifyPacket(const Pkt4Ptr& pkt);
 
-    /// @brief Performs packet processing specific to a class
+    /// @brief Performs packet processing specific to a vendor class
     ///
     /// If the selected subnet, query or response in the @c ex object is NULL
     /// this method returns immediately and returns true.
@@ -724,7 +749,7 @@ protected:
     /// @param ex The exchange holding both the client's message and the
     /// server's response.
     /// @return true if successful, false otherwise (will prevent sending response)
-    bool classSpecificProcessing(const Dhcpv4Exchange& ex);
+    bool vendorClassSpecificProcessing(const Dhcpv4Exchange& ex);
 
     /// @brief Allocation Engine.
     /// Pointer to the allocation engine that we are currently using
@@ -734,6 +759,14 @@ protected:
 
 private:
 
+    /// @brief Assign class using vendor-class-identifier option
+    ///
+    /// @note This is the first part of @ref classifyPacket
+    ///
+    /// @param pkt packet to be classified
+    /// @param classes a reference to added class names for logging
+    void classifyByVendor(const Pkt4Ptr& pkt, std::string& classes);
+
     /// @brief Constructs netmask option based on subnet4
     /// @param subnet subnet for which the netmask will be calculated
     ///

+ 1 - 0
src/bin/dhcp4/tests/Makefile.am

@@ -109,6 +109,7 @@ dhcp4_unittests_LDADD = $(top_builddir)/src/bin/dhcp4/libdhcp4.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la

+ 106 - 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,84 @@ 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));
+
+    // Check that options with other option codes are not returned.
+    for (uint8_t code = 24; code < 35; ++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(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 +2005,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 +2032,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 +2083,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 +2135,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 +2674,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 +2716,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 +2754,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 +2809,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_);
 }
 

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

@@ -410,7 +410,7 @@ TEST_F(Dhcpv4SrvTest, initResponse) {
     OptionPtr resp_sbnsel = response->getOption(DHO_SUBNET_SELECTION);
     ASSERT_TRUE(resp_sbnsel);
     OptionCustomPtr resp_custom =
-	boost::dynamic_pointer_cast<OptionCustom>(resp_sbnsel);
+        boost::dynamic_pointer_cast<OptionCustom>(resp_sbnsel);
     ASSERT_TRUE(resp_custom);
     IOAddress subnet_addr("0.0.0.0");
     ASSERT_NO_THROW(subnet_addr = resp_custom->readAddress());
@@ -1646,8 +1646,8 @@ TEST_F(Dhcpv4SrvTest, vendorOptionsDocsisDefinitions) {
     ASSERT_EQ(0, rcode_);
 }
 
-// Checks if client packets are classified properly
-TEST_F(Dhcpv4SrvTest, clientClassification) {
+// Checks if DOCSIS client packets are classified properly
+TEST_F(Dhcpv4SrvTest, docsisClientClassification) {
 
     NakedDhcpv4Srv srv(0);
 
@@ -1674,10 +1674,322 @@ TEST_F(Dhcpv4SrvTest, clientClassification) {
     EXPECT_FALSE(dis2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
 }
 
+// Checks if client packets are classified properly using match expressions.
+TEST_F(Dhcpv4SrvTest, matchClassification) {
+    IfaceMgrTestConfig test_config(true);
+    IfaceMgr::instance().openSockets4();
+
+    NakedDhcpv4Srv srv(0);
+
+    // The router class matches incoming packets with foo in a host-name
+    // option (code 12) and sets an ip-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"subnet4\": [ "
+        "{   \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+        "    \"subnet\": \"192.0.2.0/24\" } ], "
+        "\"client-classes\": [ "
+        "{   \"name\": \"router\", "
+        "    \"option-data\": ["
+        "        {    \"name\": \"ip-forwarding\", "
+        "             \"data\": \"true\" } ], "
+        "    \"test\": \"option[12].text == 'foo'\" } ] }";
+
+    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_);
+
+    CfgMgr::instance().commit();
+
+    // Create packets with enough to select the subnet
+    OptionPtr clientid = generateClientId();
+    Pkt4Ptr query1(new Pkt4(DHCPDISCOVER, 1234));
+    query1->setRemoteAddr(IOAddress("192.0.2.1"));
+    query1->addOption(clientid);
+    query1->setIface("eth1");
+    Pkt4Ptr query2(new Pkt4(DHCPDISCOVER, 1234));
+    query2->setRemoteAddr(IOAddress("192.0.2.1"));
+    query2->addOption(clientid);
+    query2->setIface("eth1");
+    Pkt4Ptr query3(new Pkt4(DHCPDISCOVER, 1234));
+    query3->setRemoteAddr(IOAddress("192.0.2.1"));
+    query3->addOption(clientid);
+    query3->setIface("eth1");
+
+    // Create and add a PRL option to the first 2 queries
+    OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+                                                 DHO_DHCP_PARAMETER_REQUEST_LIST));
+    ASSERT_TRUE(prl);
+    prl->addValue(DHO_IP_FORWARDING);
+    query1->addOption(prl);
+    query2->addOption(prl);
+
+    // Create and add a host-name option to the first and last queries
+    OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+    ASSERT_TRUE(hostname);
+    query1->addOption(hostname);
+    query3->addOption(hostname);
+
+    // Classify packets
+    srv.classifyPacket(query1);
+    srv.classifyPacket(query2);
+    srv.classifyPacket(query3);
+
+    // Packets with 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
+    Pkt4Ptr response1 = srv.processDiscover(query1);
+    Pkt4Ptr response2 = srv.processDiscover(query2);
+    Pkt4Ptr response3 = srv.processDiscover(query3);
+
+    // Classification processing should add an ip-forwarding option
+    OptionPtr opt1 = response1->getOption(DHO_IP_FORWARDING);
+    EXPECT_TRUE(opt1);
+
+    // But only for the first query: second was not classified
+    OptionPtr opt2 = response2->getOption(DHO_IP_FORWARDING);
+    EXPECT_FALSE(opt2);
+
+    // But only for the first query: third has no PRL
+    OptionPtr opt3 = response3->getOption(DHO_IP_FORWARDING);
+    EXPECT_FALSE(opt3);
+}
+
+// Checks subnet options have the priority over class options
+TEST_F(Dhcpv4SrvTest, subnetClassPriority) {
+    IfaceMgrTestConfig test_config(true);
+    IfaceMgr::instance().openSockets4();
+
+    NakedDhcpv4Srv srv(0);
+
+    // Subnet sets an ip-forwarding option in the response.
+    // The router class matches incoming packets with foo in a host-name
+    // option (code 12) and sets an ip-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"subnet4\": [ "
+        "{   \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+        "    \"subnet\": \"192.0.2.0/24\", "
+        "    \"option-data\": ["
+        "        {    \"name\": \"ip-forwarding\", "
+        "             \"data\": \"false\" } ] } ], "
+        "\"client-classes\": [ "
+        "{   \"name\": \"router\","
+        "    \"option-data\": ["
+        "        {    \"name\": \"ip-forwarding\", "
+        "             \"data\": \"true\" } ], "
+        "    \"test\": \"option[12].text == 'foo'\" } ] }";
+
+    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_);
+
+    CfgMgr::instance().commit();
+
+    // Create a packet with enough to select the subnet and go through
+    // the DISCOVER processing
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+    query->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    query->addOption(clientid);
+    query->setIface("eth1");
+
+    // Create and add a PRL option to the query
+    OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+                                                 DHO_DHCP_PARAMETER_REQUEST_LIST));
+    ASSERT_TRUE(prl);
+    prl->addValue(DHO_IP_FORWARDING);
+    query->addOption(prl);
+
+    // Create and add a host-name option to the query
+    OptionStringPtr hostname(new OptionString(Option::V4, 12, "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
+    Pkt4Ptr response = srv.processDiscover(query);
+
+    // Processing should add an ip-forwarding option
+    OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+    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 subnet options have the priority over global options
+TEST_F(Dhcpv4SrvTest, subnetGlobalPriority) {
+    IfaceMgrTestConfig test_config(true);
+    IfaceMgr::instance().openSockets4();
+
+    NakedDhcpv4Srv srv(0);
+
+    // Subnet and global set an ip-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"subnet4\": [ "
+        "{   \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+        "    \"subnet\": \"192.0.2.0/24\", "
+        "    \"option-data\": ["
+        "        {    \"name\": \"ip-forwarding\", "
+        "             \"data\": \"false\" } ] } ], "
+        "\"option-data\": ["
+        "    {    \"name\": \"ip-forwarding\", "
+        "         \"data\": \"true\" } ] }";
+
+    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_);
+
+    CfgMgr::instance().commit();
+
+    // Create a packet with enough to select the subnet and go through
+    // the DISCOVER processing
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+    query->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    query->addOption(clientid);
+    query->setIface("eth1");
+
+    // Create and add a PRL option to the query
+    OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+                                                 DHO_DHCP_PARAMETER_REQUEST_LIST));
+    ASSERT_TRUE(prl);
+    prl->addValue(DHO_IP_FORWARDING);
+    query->addOption(prl);
+
+    // Create and add a host-name option to the query
+    OptionStringPtr hostname(new OptionString(Option::V4, 12, "foo"));
+    ASSERT_TRUE(hostname);
+    query->addOption(hostname);
+
+    // Process the query
+    Pkt4Ptr response = srv.processDiscover(query);
+
+    // Processing should add an ip-forwarding option
+    OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+    ASSERT_TRUE(opt);
+    ASSERT_GT(opt->len(), opt->getHeaderLen());
+    // Global 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(Dhcpv4SrvTest, classGlobalPriority) {
+    IfaceMgrTestConfig test_config(true);
+    IfaceMgr::instance().openSockets4();
+
+    NakedDhcpv4Srv srv(0);
+
+    // A global ip-forwarding option is set in the response.
+    // The router class matches incoming packets with foo in a host-name
+    // option (code 12) and sets an ip-forwarding option in the response.
+    string config = "{ \"interfaces-config\": {"
+        "    \"interfaces\": [ \"*\" ] }, "
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"valid-lifetime\": 4000, "
+        "\"subnet4\": [ "
+        "{   \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], "
+        "    \"subnet\": \"192.0.2.0/24\" } ], "
+        "\"option-data\": ["
+        "    {    \"name\": \"ip-forwarding\", "
+        "         \"data\": \"false\" } ], "
+        "\"client-classes\": [ "
+        "{   \"name\": \"router\","
+        "    \"option-data\": ["
+        "        {    \"name\": \"ip-forwarding\", "
+        "             \"data\": \"true\" } ], "
+        "    \"test\": \"option[12].text == 'foo'\" } ] }";
+
+    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_);
+
+    CfgMgr::instance().commit();
+
+    // Create a packet with enough to select the subnet and go through
+    // the DISCOVER processing
+    Pkt4Ptr query(new Pkt4(DHCPDISCOVER, 1234));
+    query->setRemoteAddr(IOAddress("192.0.2.1"));
+    OptionPtr clientid = generateClientId();
+    query->addOption(clientid);
+    query->setIface("eth1");
+
+    // Create and add a PRL option to the query
+    OptionUint8ArrayPtr prl(new OptionUint8Array(Option::V4,
+                                                 DHO_DHCP_PARAMETER_REQUEST_LIST));
+    ASSERT_TRUE(prl);
+    prl->addValue(DHO_IP_FORWARDING);
+    query->addOption(prl);
+
+    // Create and add a host-name option to the query
+    OptionStringPtr hostname(new OptionString(Option::V4, 12, "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
+    Pkt4Ptr response = srv.processDiscover(query);
+
+    // Processing should add an ip-forwarding option
+    OptionPtr opt = response->getOption(DHO_IP_FORWARDING);
+    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 Dhcpv4SrvTest
-// .clientClassification above.
-TEST_F(Dhcpv4SrvTest, clientClassify2) {
+// .*Classification above.
+TEST_F(Dhcpv4SrvTest, clientClassify) {
 
     // This test configures 2 subnets. We actually only need the
     // first one, but since there's still this ugly hack that picks
@@ -1687,7 +1999,7 @@ TEST_F(Dhcpv4SrvTest, clientClassify2) {
 
     // The second subnet does not play any role here. The client's
     // IP address belongs to the first subnet, so only that first
-    // subnet it being tested.
+    // subnet is being tested.
     string config = "{ \"interfaces-config\": {"
         "    \"interfaces\": [ \"*\" ]"
         "},"

+ 1 - 0
src/bin/dhcp6/Makefile.am

@@ -75,6 +75,7 @@ kea_dhcp6_SOURCES  = main.cc
 
 kea_dhcp6_LDADD  = libdhcp6.la
 kea_dhcp6_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la
+kea_dhcp6_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
 kea_dhcp6_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
 kea_dhcp6_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
 kea_dhcp6_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la

+ 11 - 2
src/bin/dhcp6/dhcp6_messages.mes

@@ -84,9 +84,18 @@ successfully established a session with the Kea control channel.
 This debug message is issued just before the IPv6 DHCP server attempts
 to establish a session with the Kea control channel.
 
-% DHCP6_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
+% DHCP6_CLASS_ASSIGNED %1: client packet has been assigned to the following class(es): %2
 This debug message informs that incoming packet has been assigned to specified
-class or classes.
+class or classes. This is a normal behavior and indicates successful operation.
+The first argument specifies the client and transaction identification
+information. The second argument includes all classes to which the
+packet has been assigned.
+
+% DHCP6_CLASS_UNCONFIGURED %1: client packet belongs to an unconfigured class: %1
+This debug message informs that incoming packet belongs to a class
+which cannot be found in the configuration. Either a hook written
+before the classification was added to Kea is used, or class naming is
+inconsistent.
 
 % DHCP6_COMMAND_RECEIVED received command %1, arguments: %2
 A debug message listing the command (and possible arguments) received

+ 158 - 55
src/bin/dhcp6/dhcp6_srv.cc

@@ -42,6 +42,8 @@
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/subnet_selector.h>
 #include <dhcpsrv/utils.h>
+#include <eval/evaluate.h>
+#include <eval/eval_messages.h>
 #include <exceptions/exceptions.h>
 #include <hooks/callout_handle.h>
 #include <hooks/hooks_log.h>
@@ -882,14 +884,56 @@ 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) {
+    // First subnet configured options
+    if (ctx.subnet_ && !ctx.subnet_->getCfgOption()->empty()) {
+        co_list.push_back(ctx.subnet_->getCfgOption());
+    }
+
+    // Each class in the incoming packet
+    const ClientClasses& classes = question->getClasses();
+    for (ClientClasses::const_iterator cclass = classes.begin();
+         cclass != classes.end(); ++cclass) {
+        // Find the client class definition for this class
+        const ClientClassDefPtr& ccdef = CfgMgr::instance().getCurrentCfg()->
+            getClientClassDictionary()->findClass(*cclass);
+        if (!ccdef) {
+            // Not found: the class is not configured
+            if (((*cclass).size() <= VENDOR_CLASS_PREFIX.size()) ||
+                ((*cclass).compare(0, VENDOR_CLASS_PREFIX.size(), VENDOR_CLASS_PREFIX) != 0)) {
+                // Not a VENDOR_CLASS_* so should be configured
+                LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_UNCONFIGURED)
+                    .arg(question->getLabel())
+                    .arg(*cclass);
+            }
+            // Skip it
+            continue;
+        }
+        if (ccdef->getCfgOption()->empty()) {
+            // Skip classes which don't configure options
+            continue;
+        }
+        co_list.push_back(ccdef->getCfgOption());
+    }
+
+    // Last global options
+    if (!CfgMgr::instance().getCurrentCfg()->getCfgOption()->empty()) {
+        co_list.push_back(CfgMgr::instance().getCurrentCfg()->getCfgOption());
+    }
+}
+
+void
 Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
-                                  AllocEngine::ClientContext6& ctx) {
+                                  const CfgOptionList& co_list) {
 
     // Client requests some options using ORO option. Try to
     // get this option from client's message.
@@ -898,44 +942,31 @@ Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer,
         (question->getOption(D6O_ORO));
 
     // Option ORO not found? We're done here then.
-    if (!option_oro) {
+    if (!option_oro || co_list.empty()) {
         return;
     }
 
-    // Get global option definitions (i.e. options defined to apply to all,
-    // unless overwritten on a subnet or host level)
-    ConstCfgOptionPtr global_opts = CfgMgr::instance().getCurrentCfg()->
-        getCfgOption();
-
     // Get the list of options that client requested.
     const std::vector<uint16_t>& requested_opts = option_oro->getValues();
     BOOST_FOREACH(uint16_t opt, requested_opts) {
-        // If we found a subnet for this client, all options (including the
-        // global options) should be available through the options
-        // configuration for the subnet.
-        if (ctx.subnet_) {
-            OptionDescriptor desc = ctx.subnet_->getCfgOption()->get("dhcp6",
-                                                                     opt);
-            if (desc.option_) {
-                // Attempt to assign an option from subnet first.
-                answer->addOption(desc.option_);
-                continue;
-            }
-
-        // If there is no subnet selected (e.g. Information-request message
-        // case) we need to look at the global options.
-        } else {
-            OptionDescriptor desc = global_opts->get("dhcp6", opt);
+        // Iterate on the configured option list
+        for (CfgOptionList::const_iterator copts = co_list.begin();
+             copts != co_list.end(); ++copts) {
+            OptionDescriptor desc = (*copts)->get("dhcp6", opt);
+            // Got it: add it and jump to the outer loop
             if (desc.option_) {
                 answer->addOption(desc.option_);
+                break;
             }
         }
     }
 }
 
 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
@@ -949,7 +980,7 @@ Dhcpv6Srv::appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer
     // 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) {
+    if (!vendor_req || co_list.empty()) {
         return;
     }
 
@@ -972,10 +1003,14 @@ 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) {
-        OptionDescriptor desc = ctx.subnet_->getCfgOption()->get(vendor_id, opt);
-        if (desc.option_) {
-            vendor_rsp->addOption(desc.option_);
-            added = true;
+        for (CfgOptionList::const_iterator copts = co_list.begin();
+             copts != co_list.end(); ++copts) {
+            OptionDescriptor desc = (*copts)->get(vendor_id, opt);
+            if (desc.option_) {
+                vendor_rsp->addOption(desc.option_);
+                added = true;
+                break;
+            }
         }
     }
 
@@ -2297,9 +2332,11 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     }
 
     copyClientOptions(solicit, response);
-    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, co_list);
+    appendRequestedVendorOptions(solicit, response, ctx, co_list);
 
     processClientFqdn(solicit, response, ctx);
     assignLeases(solicit, response, ctx);
@@ -2324,9 +2361,11 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, request->getTransid()));
 
     copyClientOptions(request, reply);
-    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, co_list);
+    appendRequestedVendorOptions(request, reply, ctx, co_list);
 
     processClientFqdn(request, reply, ctx);
     assignLeases(request, reply, ctx);
@@ -2347,8 +2386,10 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, renew->getTransid()));
 
     copyClientOptions(renew, reply);
-    appendDefaultOptions(renew, reply);
-    appendRequestedOptions(renew, reply, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(renew, ctx, co_list);
+    appendDefaultOptions(renew, reply, co_list);
+    appendRequestedOptions(renew, reply, co_list);
 
     processClientFqdn(renew, reply, ctx);
     extendLeases(renew, reply, ctx);
@@ -2369,8 +2410,10 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, rebind->getTransid()));
 
     copyClientOptions(rebind, reply);
-    appendDefaultOptions(rebind, reply);
-    appendRequestedOptions(rebind, reply, ctx);
+    CfgOptionList co_list;
+    buildCfgOptionList(rebind, ctx, co_list);
+    appendDefaultOptions(rebind, reply, co_list);
+    appendRequestedOptions(rebind, reply, co_list);
 
     processClientFqdn(rebind, reply, ctx);
     extendLeases(rebind, reply, ctx);
@@ -2399,7 +2442,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);
-    appendDefaultOptions(confirm, reply);
+    CfgOptionList co_list;
+    buildCfgOptionList(confirm, ctx, co_list);
+    appendDefaultOptions(confirm, reply, co_list);
+    appendRequestedOptions(confirm, reply, 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.
@@ -2478,7 +2524,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);
 
@@ -2503,8 +2551,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);
@@ -2778,13 +2830,17 @@ Dhcpv6Srv::processInfRequest(const Pkt6Ptr& inf_request) {
     // Copy client options (client-id, also relay information if present)
     copyClientOptions(inf_request, reply);
 
+    // Build the configured option list for append methods
+    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, co_list);
 
     return (reply);
 }
@@ -2891,7 +2947,7 @@ Dhcpv6Srv::unpackOptions(const OptionBuffer& buf,
     return (offset);
 }
 
-void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
+void Dhcpv6Srv::classifyByVendor(const Pkt6Ptr& pkt, std::string& classes) {
     OptionVendorClassPtr vclass = boost::dynamic_pointer_cast<
         OptionVendorClass>(pkt->getOption(D6O_VENDOR_CLASS));
 
@@ -2899,22 +2955,69 @@ void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
         return;
     }
 
-    std::ostringstream classes;
     if (vclass->hasTuple(DOCSIS3_CLASS_MODEM)) {
-        classes << VENDOR_CLASS_PREFIX << DOCSIS3_CLASS_MODEM;
+        pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM);
+        classes += VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_MODEM + " ";
 
     } else if (vclass->hasTuple(DOCSIS3_CLASS_EROUTER)) {
-        classes << VENDOR_CLASS_PREFIX << DOCSIS3_CLASS_EROUTER;
+        pkt->addClass(VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER);
+        classes += VENDOR_CLASS_PREFIX + DOCSIS3_CLASS_EROUTER + " ";
 
     } else {
-        classes << VENDOR_CLASS_PREFIX << vclass->getTuple(0).getText();
+        pkt->addClass(VENDOR_CLASS_PREFIX + vclass->getTuple(0).getText());
+        classes + VENDOR_CLASS_PREFIX + vclass->getTuple(0).getText() + " ";
+    }
+}
+
+void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
+    string classes = "";
+
+    // First phase: built-in vendor class processing
+    classifyByVendor(pkt, classes);
+
+    // Run match expressions
+    // Note getClientClassDictionary() cannot be null
+    const ClientClassDefMapPtr& defs_ptr = CfgMgr::instance().getCurrentCfg()->
+        getClientClassDictionary()->getClasses();
+    for (ClientClassDefMap::const_iterator it = defs_ptr->begin();
+         it != defs_ptr->end(); ++it) {
+        // Note second cannot be null
+        const ExpressionPtr& expr_ptr = it->second->getMatchExpr();
+        // Nothing to do without an expression to evaluate
+        if (!expr_ptr) {
+            continue;
+        }
+        // Evaluate the expression which can return false (no match),
+        // true (match) or raise an exception (error)
+        try {
+            bool status = evaluate(*expr_ptr, *pkt);
+            if (status) {
+                LOG_INFO(dhcp6_logger, EVAL_RESULT)
+                    .arg(it->first)
+                    .arg(status);
+                // Matching: add the class
+                pkt->addClass(it->first);
+                classes += it->first + " ";
+            } else {
+                LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, EVAL_RESULT)
+                    .arg(it->first)
+                    .arg(status);
+            }
+        } catch (const Exception& ex) {
+            LOG_ERROR(dhcp6_logger, EVAL_RESULT)
+                .arg(it->first)
+                .arg(ex.what());
+        } catch (...) {
+            LOG_ERROR(dhcp6_logger, EVAL_RESULT)
+                .arg(it->first)
+                .arg("get exception?");
+        }
     }
 
-    // If there is no class identified, leave.
-    if (!classes.str().empty()) {
-        pkt->addClass(classes.str());
+    if (!classes.empty()) {
         LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_ASSIGNED)
-            .arg(classes.str());
+            .arg(pkt->getLabel())
+            .arg(classes);
     }
 }
 

+ 34 - 7
src/bin/dhcp6/dhcp6_srv.h

@@ -24,6 +24,7 @@
 #include <dhcp/option_definition.h>
 #include <dhcp/pkt6.h>
 #include <dhcpsrv/alloc_engine.h>
+#include <dhcpsrv/cfg_option.h>
 #include <dhcpsrv/d2_client_mgr.h>
 #include <dhcpsrv/subnet.h>
 #include <hooks/callout_handle.h>
@@ -428,6 +429,18 @@ protected:
     /// @param answer server's message (options will be copied here)
     void copyClientOptions(const Pkt6Ptr& question, Pkt6Ptr& answer);
 
+    /// @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 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,
+                            CfgOptionList& co_list);
+
     /// @brief Appends default options to server's answer.
     ///
     /// Adds required options to server's answer. In particular, server-id
@@ -436,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.
     ///
@@ -444,9 +459,9 @@ 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);
+                                const CfgOptionList& co_list);
 
     /// @brief Appends requested vendor options to server's answer.
     ///
@@ -456,8 +471,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.
     ///
@@ -633,9 +650,11 @@ protected:
 
     /// @brief Assigns incoming packet to zero or more classes.
     ///
-    /// @note For now, the client classification is very simple. It just uses
-    /// content of the vendor-class-identifier option as a class. The resulting
-    /// class will be stored in packet (see @ref isc::dhcp::Pkt6::classes_ and
+    /// @note This is done in two phases: first the content of the
+    /// vendor-class-identifier option is used as a class, by
+    /// calling @ref classifyByVendor(). Second classification match
+    /// expressions are evaluated. The resulting classes will be stored
+    /// in the packet (see @ref isc::dhcp::Pkt6::classes_ and
     /// @ref isc::dhcp::Pkt6::inClass).
     ///
     /// @param pkt packet to be classified
@@ -739,6 +758,14 @@ protected:
 
 private:
 
+    /// @brief Assign class using vendor-class-identifier option
+    ///
+    /// @note This is the first part of @ref classifyPacket
+    ///
+    /// @param pkt packet to be classified
+    /// @param classes a reference to added class names for logging
+    void classifyByVendor(const Pkt6Ptr& pkt, std::string& classes);
+
     /// @brief Generate FQDN to be sent to a client if none exists.
     ///
     /// This function is meant to be called by the functions which process

+ 1 - 0
src/bin/dhcp6/tests/Makefile.am

@@ -112,6 +112,7 @@ dhcp6_unittests_LDADD = $(top_builddir)/src/bin/dhcp6/libdhcp6.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/cfgrpt/libcfgrpt.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libkea-dhcpsrv.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/testutils/libdhcpsrvtest.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/eval/libkea-eval.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libkea-dhcp_ddns.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/stats/libkea-stats.la

+ 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_);
 }
 

+ 314 - 6
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,318 @@ 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].text == '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(DHCPV6_SOLICIT, 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(DHCPV6_SOLICIT, 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 with 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 query: second was not classified
+    OptionPtr opt2 = response2->getOption(2345);
+    EXPECT_FALSE(opt2);
+
+    // But only for the first query: 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].text == '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 an 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);
+
+    // 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 subnet options have the priority over global options
+TEST_F(Dhcpv6SrvTest, subnetGlobalPriority) {
+    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\" }],"
+        "\"option-data\": ["
+        "    {    \"name\": \"ipv6-forwarding\", "
+        "         \"data\": \"false\" } ], "
+        "\"subnet6\": [ "
+        "{   \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], "
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"eth1\", "
+        "    \"option-data\": ["
+        "        {    \"name\": \"ipv6-forwarding\", "
+        "             \"data\": \"false\" } ] } ] }";
+    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 an 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);
+
+    // Process the query
+    Pkt6Ptr response = srv.processSolicit(query);
+
+    // Processing should add an ip-forwarding option
+    OptionPtr opt = response->getOption(2345);
+    ASSERT_TRUE(opt);
+    ASSERT_GT(opt->len(), opt->getHeaderLen());
+    // Global 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].text == '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 an 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);
+
+    // 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
@@ -1849,7 +2157,7 @@ TEST_F(Dhcpv6SrvTest, clientClassify2) {
 
     // The second subnet does not play any role here. The client's
     // IP address belongs to the first subnet, so only that first
-    // subnet it being tested.
+    // subnet is being tested.
     string config = "{ \"interfaces-config\": {"
         "  \"interfaces\": [ \"*\" ]"
         "},"
@@ -1895,7 +2203,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.

+ 6 - 0
src/lib/dhcp/pkt.h

@@ -237,6 +237,12 @@ public:
     /// @param client_class name of the class to be added
     void addClass(const isc::dhcp::ClientClass& client_class);
 
+    /// @brief Returns the class set
+    ///
+    /// @note This should be used only to iterate over the class set.
+    /// @return
+    const ClientClasses& getClasses() const { return (classes_); }
+
     /// @brief Unparsed data (in received packets).
     ///
     /// @warning This public member is accessed by derived

+ 31 - 1
src/lib/dhcp/tests/classify_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2015 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
@@ -85,3 +85,33 @@ TEST(ClassifyTest, ClientClassesFromString) {
         EXPECT_TRUE(classes.empty());
     }
 }
+
+// Check if one can iterate over a ClientClasses object
+TEST(ClassifyTest, ClientClassesIterator) {
+    ClientClasses classes("alpha, beta, gamma");
+    size_t count = 0;
+    bool seenalpha = false;
+    bool seenbeta = false;
+    bool seengamma = false;
+    bool seendelta = false;
+    for (ClientClasses::const_iterator it = classes.begin();
+         it != classes.end(); ++it) {
+        ++count;
+        if (*it == "alpha") {
+            seenalpha = true;
+        } else if (*it == "beta") {
+            seenbeta = true;
+        } else if (*it == "gamma") {
+            seengamma = true;
+        } else if (*it == "delta") {
+            seendelta = true;
+        } else {
+            ADD_FAILURE() << "Got unexpected " << *it;
+        }
+    }
+    EXPECT_EQ(count, classes.size());
+    EXPECT_TRUE(seenalpha);
+    EXPECT_TRUE(seenbeta);
+    EXPECT_TRUE(seengamma);
+    EXPECT_FALSE(seendelta);
+}

+ 5 - 0
src/lib/dhcpsrv/cfg_option.cc

@@ -32,6 +32,11 @@ CfgOption::CfgOption() {
 }
 
 bool
+CfgOption::empty() const {
+    return (options_.empty() && vendor_options_.empty());
+}
+
+bool
 CfgOption::equals(const CfgOption& other) const {
     return (options_.equals(other.options_) &&
             vendor_options_.equals(other.vendor_options_));

+ 11 - 0
src/lib/dhcpsrv/cfg_option.h

@@ -27,6 +27,7 @@
 #include <stdint.h>
 #include <string>
 #include <set>
+#include <list>
 
 namespace isc {
 namespace dhcp {
@@ -201,6 +202,11 @@ public:
     /// @brief default constructor
     CfgOption();
 
+    /// @brief Indicates the object is empty
+    ///
+    /// @return true when the object is empty
+    bool empty() const;
+
     /// @name Methods and operators used for comparing objects.
     ///
     //@{
@@ -263,6 +269,8 @@ public:
     /// as a parameter. If an item exists in the destination it is not
     /// copied.
     ///
+    /// @note: this method is not longer used so should become private.
+    ///
     /// @param [out] other Configuration object to merge to.
     void mergeTo(CfgOption& other) const;
 
@@ -407,6 +415,9 @@ typedef boost::shared_ptr<CfgOption> CfgOptionPtr;
 /// @brief Const pointer.
 typedef boost::shared_ptr<const CfgOption> ConstCfgOptionPtr;
 
+/// @brief Const pointer list.
+typedef std::list<ConstCfgOptionPtr> CfgOptionList;
+
 //@}
 
 }

+ 8 - 1
src/lib/dhcpsrv/option_space_container.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2015 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
@@ -39,6 +39,13 @@ public:
     /// Pointer to the container.
     typedef boost::shared_ptr<ContainerType> ItemsContainerPtr;
 
+    /// @brief Indicates the container is empty
+    ///
+    /// @return true when the container is empty
+    bool empty() const {
+        return (option_space_map_.empty());
+    }
+
     /// @brief Adds a new item to the option_space.
     ///
     /// @param item reference to the item being added.

+ 11 - 5
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 ****************************
@@ -1256,12 +1262,12 @@ SubnetConfigParser::createSubnet() {
         subnet_->setIface(iface);
     }
 
-    // Merge globally defined options to the subnet specific options.
-    CfgMgr::instance().getStagingCfg()->getCfgOption()->mergeTo(*options_);
-    // Copy all options to the subnet configuration.
+    // Here globally defined options were merged to the subnet specific
+    // options but this is no longer the case (they have a different
+    // and not consecutive priority).
+
+    // 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:

+ 21 - 0
src/lib/dhcpsrv/tests/cfg_option_unittest.cc

@@ -28,6 +28,27 @@ using namespace isc::dhcp;
 
 namespace {
 
+// This test verifies the empty predicate.
+TEST(CfgOptionTest, empty) {
+    CfgOption cfg1;
+    CfgOption cfg2;
+
+    // Initially the option configurations should be empty.
+    ASSERT_TRUE(cfg1.empty());
+    ASSERT_TRUE(cfg2.empty());
+
+    // Add an option to each configuration
+    OptionPtr option(new Option(Option::V6, 1));
+    ASSERT_NO_THROW(cfg1.add(option, false, "dhcp6"));
+    ASSERT_NO_THROW(cfg2.add(option, true, "isc"));
+
+    // The first option configuration has an option
+    ASSERT_FALSE(cfg1.empty());
+
+    // The second option configuration has a vendor option
+    ASSERT_FALSE(cfg2.empty());
+}
+
 // This test verifies that the option configurations can be compared.
 TEST(CfgOptionTest, equals) {
     CfgOption cfg1;

+ 1 - 1
src/lib/eval/location.hh

@@ -1,4 +1,4 @@
-// Generated 20151120
+// Generated 20151125
 // A Bison parser, made by GNU Bison 3.0.4.
 
 // Locations for Bison parsers in C++

+ 1 - 1
src/lib/eval/position.hh

@@ -1,4 +1,4 @@
-// Generated 20151120
+// Generated 20151125
 // A Bison parser, made by GNU Bison 3.0.4.
 
 // Positions for Bison parsers in C++

+ 1 - 1
src/lib/eval/stack.hh

@@ -1,4 +1,4 @@
-// Generated 20151120
+// Generated 20151125
 // A Bison parser, made by GNU Bison 3.0.4.
 
 // Stack handling for Bison parsers in C++