Browse Source

[master] Merge branch 'trac4765'

Marcin Siodelski 8 years ago
parent
commit
242fbc47b3

+ 8 - 1
doc/examples/kea4/classify.json

@@ -73,10 +73,17 @@
     },
 # This one doesn't have any client-class specified, so everyone
 # is allowed in. The normal subnet selection rules still apply,
-# though.      
+# though. There is also a static class reservation for a client
+# using MAC address 1a:1b:1c:1d:1e:1f. This client will always
+# be assigned to this class.
     {
         "pools": [ { "pool":  "192.0.3.1 - 192.0.3.200" } ],
         "subnet": "192.0.3.0/24",
+        "reservations": [
+        {
+            "hw-address": "1a:1b:1c:1d:1e:1f",
+            "client-classes": [ "VoIP" ]
+        } ],
         "interface": "ethX"
     }
   ]

+ 8 - 0
doc/examples/kea6/classify.json

@@ -58,9 +58,17 @@
         "client-class": "cable-modems",
         "interface": "ethX"
     },
+# The following subnet contains a class reservation for a client using
+# DUID 01:02:03:04:05:0A:0B:0C:0D:0E. This client will always be assigned
+# to this class.
     {
         "pools": [ { "pool": "2001:db8:2::/80" } ],
         "subnet": "2001:db8:2::/64",
+        "reservations": [
+        {
+            "duid": "01:02:03:04:05:0A:0B:0C:0D:0E",
+            "client-classes": [ "cable-modems" ]
+        } ],
         "interface": "ethX"
     }
   ]

+ 8 - 0
doc/guide/classify.xml

@@ -94,6 +94,14 @@
       </note>
   </section>
 
+  <section id="classification-using-host-reservations">
+    <title>Using Static Host Reservations In Classification</title>
+    <para>Classes can be statically assigned to the clients using techniques described
+    in <xref linkend="reservation4-client-classes"/> and
+    <xref linkend="reservation6-client-classes"/>.
+    </para>
+  </section>
+
   <section id="classification-using-vendor">
     <title>Using Vendor Class Information In Classification</title>
       <para>

+ 55 - 0
doc/guide/dhcp4-srv.xml

@@ -2931,6 +2931,61 @@ It is merely echoed by the server
     them can be omitted.</para>
     </section>
 
+    <section id="reservation4-client-classes">
+      <title>Reserving Client Classes in DHCPv4</title>
+      <para>The <xref linkend="classification-using-expressions"/> explains how
+      to configure the server to assign classes to a client based on the content
+      of the options that this client sends to the server. Host reservations
+      mechanisms also allow for statically assigning classes to the clients.
+      The definitions of these classes must exist in the Kea
+      configuration. The following configuration snippet shows how to specify
+      that the client belongs to classes <command>reserved-class1</command>
+      and <command>reserved-class2</command>. Those classes are associated with
+      specific options being sent to the clients which belong to them.
+      </para>
+
+<screen>
+{
+    "client-classes": [
+    {
+       "name": "reserved-class1",
+       "option-data": [
+       {
+           "name": "routers",
+           "data": "10.0.0.200"
+       }
+       ]
+    },
+    {
+       "name": "reserved-class2",
+       "option-data": [
+       {
+           "name": "domain-name-servers",
+           "data": "10.0.0.201"
+       }
+       ]
+    }
+    ],
+    "subnet4": [ {
+        "subnet": "10.0.0.0/24",
+        "pools": [ { "pool": "10.0.0.10-10.0.0.100" } ],
+        "reservations": [
+        {
+            "hw-address": "aa:bb:cc:dd:ee:ff",
+            <userinput>
+            "client-classes": [ "reserved-class1", "reserved-class2" ]
+            </userinput>
+        }
+        ]
+    } ]
+}
+
+</screen>
+
+    <para>Static class assignments, as shown above, can be used in conjuction
+    with classification using expressions.</para>
+    </section>
+
     <section id="reservations4-mysql-pgsql">
       <title>Storing host reservations in MySQL or PostgreSQL</title>
 

+ 52 - 0
doc/guide/dhcp6-srv.xml

@@ -2591,6 +2591,58 @@ should include options from the isc option space:
 
     </section>
 
+    <section id="reservation6-client-classes">
+      <title>Reserving Client Classes in DHCPv6</title>
+      <para>The <xref linkend="classification-using-expressions"/> explains how
+      to configure the server to assign classes to a client based on the content
+      of the options that this client sends to the server. Host reservations
+      mechanisms also allow for statically assigning classes to the clients.
+      The definitions of these classes must exist in the Kea
+      configuration. The following configuration snippet shows how to specify
+      that the client belongs to classes <command>reserved-class1</command>
+      and <command>reserved-class2</command>. Those classes are associated with
+      specific options being sent to the clients which belong to them.
+      </para>
+<screen>
+{
+    "client-classes": [
+    {
+       "name": "reserved-class1",
+       "option-data": [
+       {
+           "name": "dns-servers",
+           "data": "2001:db8:1::50"
+       }
+       ]
+   },
+   {
+       "name": "reserved-class2",
+       "option-data": [
+       {
+           "name": "nis-servers",
+           "data": "2001:db8:1::100"
+       }
+       ]
+    }
+    ],
+    "subnet6": [
+    {   "pools": [ { "pool": "2001:db8:1::/64" } ],
+        "subnet": "2001:db8:1::/48",
+        "reservations": [
+        {
+            "duid": "01:02:03:04:05:06:07:08",
+            <userinput>
+            "client-classes": [ "reserved-class1", "reserved-class2" ]
+            </userinput>
+         } ]
+     } ]
+ }
+
+</screen>
+    <para>Static class assignments, as shown above, can be used in conjuction
+    with classification using expressions.</para>
+    </section>
+
     <section id="reservations6-mysql-pgsql">
       <title>Storing host reservations in MySQL or PostgreSQL</title>
 

+ 15 - 0
src/bin/dhcp4/dhcp4.spec

@@ -531,6 +531,21 @@
                         "item_type": "string",
                         "item_optional": true,
                         "item_default": ""
+                      },
+                      {
+                        "item_name": "client-classes",
+                        "item_type": "list",
+                        "item_optional": true,
+                        "item_default": [],
+                        "item_description": "list of reserved classes for a client",
+                        "list_item_spec":
+                        {
+                          "item_name": "client-class",
+                          "item_type": "string",
+                          "item_optional": false,
+                          "item_default": "",
+                          "item_description": "one of the classes reserved for a client"
+                        }
                       } ]
                   }
                 },

+ 24 - 12
src/bin/dhcp4/dhcp4_srv.cc

@@ -57,6 +57,7 @@
 #endif
 #include <dhcpsrv/memfile_lease_mgr.h>
 
+#include <boost/algorithm/string/join.hpp>
 #include <boost/bind.hpp>
 #include <boost/foreach.hpp>
 #include <boost/shared_ptr.hpp>
@@ -153,8 +154,19 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine,
 
             // Check for static reservations.
             alloc_engine->findReservation(*context_);
+
+            // Assign classes.
+            setReservedClientClasses();
         }
     }
+
+    const ClientClasses& classes = query_->getClasses();
+    if (!classes.empty()) {
+        std::string joined_classes = boost::algorithm::join(classes, ", ");
+        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_ASSIGNED)
+            .arg(query_->getLabel())
+            .arg(joined_classes);
+    }
 };
 
 void
@@ -334,6 +346,16 @@ Dhcpv4Exchange::setHostIdentifiers() {
 }
 
 void
+Dhcpv4Exchange::setReservedClientClasses() {
+    if (context_->host_ && query_) {
+        BOOST_FOREACH(const std::string& client_class,
+                      context_->host_->getClientClasses4()) {
+            query_->addClass(client_class);
+        }
+    }
+}
+
+void
 Dhcpv4Exchange::setReservedMessageFields() {
     ConstHostPtr host = context_->host_;
     // Nothing to do if host reservations not specified for this client.
@@ -2659,7 +2681,7 @@ Dhcpv4Srv::sanityCheck(const Pkt4Ptr& query, RequirementLevel serverid) {
     }
 }
 
-void Dhcpv4Srv::classifyByVendor(const Pkt4Ptr& pkt, std::string& classes) {
+void Dhcpv4Srv::classifyByVendor(const Pkt4Ptr& pkt) {
     // Built-in vendor class processing
     boost::shared_ptr<OptionString> vendor_class =
         boost::dynamic_pointer_cast<OptionString>(pkt->getOption(DHO_VENDOR_CLASS_IDENTIFIER));
@@ -2669,14 +2691,11 @@ void Dhcpv4Srv::classifyByVendor(const Pkt4Ptr& pkt, std::string& classes) {
     }
 
     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);
+    classifyByVendor(pkt);
 
     // Run match expressions
     // Note getClientClassDictionary() cannot be null
@@ -2700,7 +2719,6 @@ void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
                     .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)
@@ -2716,12 +2734,6 @@ void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
                 .arg("get exception?");
         }
     }
-
-    if (!classes.empty()) {
-        LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_ASSIGNED)
-            .arg(pkt->getLabel())
-            .arg(classes);
-    }
 }
 
 void

+ 4 - 2
src/bin/dhcp4/dhcp4_srv.h

@@ -154,6 +154,9 @@ private:
     /// host-reservation-identifiers
     void setHostIdentifiers();
 
+    /// @brief Assigns classes retrieved from host reservation database.
+    void setReservedClientClasses();
+
     /// @brief Pointer to the allocation engine used by the server.
     AllocEnginePtr alloc_engine_;
     /// @brief Pointer to the DHCPv4 message sent by the client.
@@ -804,8 +807,7 @@ private:
     /// @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);
+    void classifyByVendor(const Pkt4Ptr& pkt);
 
     /// @private
     /// @brief Constructs netmask option based on subnet4

+ 112 - 2
src/bin/dhcp4/tests/classify_unittest.cc

@@ -24,7 +24,7 @@ namespace {
 /// @brief Set of JSON configurations used throughout the classify tests.
 ///
 /// - Configuration 0:
-///   - Used for testing direct traffic
+///   - Used for testing dynamic assignment of client classes
 ///   - 1 subnet: 10.0.0.0/24
 ///   - 1 pool: 10.0.0.10-10.0.0.100
 ///   - the following classes defined:
@@ -32,6 +32,17 @@ namespace {
 ///     option[93].hex == 0x0007, set server-hostname to deneb
 ///     option[93].hex == 0x0006, set boot-file-name to pxelinux.0
 ///     option[93].hex == 0x0001, set boot-file-name to ipxe.efi
+///
+/// - Configuration 1:
+///   - Used for testing reservations of client classes for a client
+///   - The following classes are defined:
+///     - 'pxe', next-server set to 1.2.3.4, assigned dynamically
+///     - 'reserved-class1', routers set to 10.0.0.200, reserved for a
+///        host using HW address 'aa:bb:cc:dd:ee:ff'
+///     - 'reserved-class2', domain-name-servers set to 10.0.0.201,
+///        also reserved for the host using HW address
+///        'aa:bb:cc:dd:ee:ff'
+///   - Subnet of 10.0.0.0/24 with a single address pool
 const char* CONFIGS[] = {
     // Configuration 0
     "{ \"interfaces-config\": {"
@@ -65,8 +76,50 @@ const char* CONFIGS[] = {
         "    \"id\": 1,"
         "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ]"
         " } ]"
-    "}"
+    "}",
 
+    // Configuration 1
+    "{ \"interfaces-config\": {"
+        "   \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"valid-lifetime\": 600,"
+        "\"client-classes\": ["
+        "{"
+        "   \"name\": \"pxe\","
+        "   \"test\": \"option[93].hex == 0x0009\","
+        "   \"next-server\": \"1.2.3.4\""
+        "},"
+        "{"
+        "   \"name\": \"reserved-class1\","
+        "   \"option-data\": ["
+        "   {"
+        "       \"name\": \"routers\","
+        "       \"data\": \"10.0.0.200\""
+        "   }"
+        "   ]"
+        "},"
+        "{"
+        "   \"name\": \"reserved-class2\","
+        "   \"option-data\": ["
+        "   {"
+        "       \"name\": \"domain-name-servers\","
+        "       \"data\": \"10.0.0.201\""
+        "   }"
+        "   ]"
+        "}"
+        "],"
+        "\"subnet4\": [ { "
+        "    \"subnet\": \"10.0.0.0/24\", "
+        "    \"id\": 1,"
+        "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+        "    \"reservations\": [ "
+        "    {"
+        "        \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+        "        \"client-classes\": [ \"reserved-class1\", \"reserved-class2\" ]"
+        "    }"
+        "    ]"
+        " } ]"
+    "}"
 };
 
 /// @brief Test fixture class for testing classification.
@@ -267,5 +320,62 @@ TEST_F(ClassifyTest, fixedFieldsInformFile2) {
     testFixedFields(CONFIGS[0], DHCPINFORM, pxe, "0.0.0.0", "", "ipxe.efi");
 }
 
+// This test checks that it is possible to specify static reservations for
+// client classes.
+TEST_F(ClassifyTest, clientClassesInHostReservations) {
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Initially, the client uses hardware address for which there are
+    // no reservations.
+    client.setHWAddress("aa:bb:cc:dd:ee:fe");
+    // DNS servers have to be requested to be returned.
+    client.requestOptions(DHO_DOMAIN_NAME_SERVERS);
+
+    // Add option 93 that matches 'pxe' class in the configuration.
+    OptionPtr pxe(new OptionInt<uint16_t>(Option::V4, 93, 0x0009));
+    client.addExtraOption(pxe);
+
+    // Configure DHCP server.
+    configure(CONFIGS[1], *client.getServer());
+
+    // Perform 4-way exchange. The client's HW address doesn't match the
+    // reservations, so we expect that only 'pxe' class will be matched.
+    ASSERT_NO_THROW(client.doDORA());
+
+    ASSERT_TRUE(client.getContext().response_);
+    Pkt4Ptr resp = client.getContext().response_;
+
+    // 'pxe' class matches so the siaddr should be set appropriately.
+    EXPECT_EQ("1.2.3.4", resp->getSiaddr().toText());
+    // This client has no reservations for the classes associated with
+    // DNS servers and Routers options.
+    EXPECT_EQ(0, client.config_.routers_.size());
+    EXPECT_EQ(0, client.config_.dns_servers_.size());
+
+    // Modify HW address to match the reservations.
+    client.setHWAddress("aa:bb:cc:dd:ee:ff");
+    ASSERT_NO_THROW(client.doDORA());
+
+    ASSERT_TRUE(client.getContext().response_);
+    resp = client.getContext().response_;
+
+    // This time, the client matches 3 classes (for two it has reservations).
+    EXPECT_EQ("1.2.3.4", resp->getSiaddr().toText());
+    EXPECT_EQ(1, client.config_.routers_.size());
+    EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+    EXPECT_EQ(1, client.config_.dns_servers_.size());
+    EXPECT_EQ("10.0.0.201", client.config_.dns_servers_[0].toText());
+
+    // This should also work for DHCPINFORM case.
+    ASSERT_NO_THROW(client.doInform());
+    ASSERT_TRUE(client.getContext().response_);
+    resp = client.getContext().response_;
+
+    EXPECT_EQ("1.2.3.4", resp->getSiaddr().toText());
+    EXPECT_EQ(1, client.config_.routers_.size());
+    EXPECT_EQ("10.0.0.200", client.config_.routers_[0].toText());
+    EXPECT_EQ(1, client.config_.dns_servers_.size());
+    EXPECT_EQ("10.0.0.201", client.config_.dns_servers_[0].toText());
+}
+
 
 } // end of anonymous namespace

+ 15 - 0
src/bin/dhcp6/dhcp6.spec

@@ -604,6 +604,21 @@
                             "item_optional": false,
                             "item_default": ""
                         }
+                      },
+                      {
+                        "item_name": "client-classes",
+                        "item_type": "list",
+                        "item_optional": true,
+                        "item_default": [],
+                        "item_description": "list of reserved classes for a client",
+                        "list_item_spec":
+                        {
+                          "item_name": "client-class",
+                          "item_type": "string",
+                          "item_optional": false,
+                          "item_default": "",
+                          "item_description": "one of the classes reserved for a client"
+                        }
                       } ]
                   }
                 },

+ 24 - 1
src/bin/dhcp6/dhcp6_srv.cc

@@ -67,6 +67,8 @@
 #include <boost/foreach.hpp>
 #include <boost/tokenizer.hpp>
 #include <boost/algorithm/string/erase.hpp>
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/split.hpp>
 
 #include <stdlib.h>
 #include <time.h>
@@ -2273,6 +2275,7 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(solicit, ctx);
+    setReservedClientClasses(solicit, ctx);
 
     Pkt6Ptr response(new Pkt6(DHCPV6_ADVERTISE, solicit->getTransid()));
 
@@ -2318,6 +2321,7 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(request, ctx);
+    setReservedClientClasses(request, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, request->getTransid()));
 
@@ -2344,6 +2348,7 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(renew, ctx);
+    setReservedClientClasses(renew, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, renew->getTransid()));
 
@@ -2370,6 +2375,7 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(rebind, ctx);
+    setReservedClientClasses(rebind, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, rebind->getTransid()));
 
@@ -2396,6 +2402,7 @@ Dhcpv6Srv::processConfirm(const Pkt6Ptr& confirm) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(confirm, ctx);
+    setReservedClientClasses(confirm, ctx);
 
     // Get IA_NAs from the Confirm. If there are none, the message is
     // invalid and must be discarded. There is nothing more to do.
@@ -2488,6 +2495,7 @@ Dhcpv6Srv::processRelease(const Pkt6Ptr& release) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(release, ctx);
+    setReservedClientClasses(release, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, release->getTransid()));
 
@@ -2516,6 +2524,7 @@ Dhcpv6Srv::processDecline(const Pkt6Ptr& decline) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(decline, ctx);
+    setReservedClientClasses(decline, ctx);
 
     // Copy client options (client-id, also relay information if present)
     copyClientOptions(decline, reply);
@@ -2796,6 +2805,7 @@ Dhcpv6Srv::processInfRequest(const Pkt6Ptr& inf_request) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(inf_request, ctx);
+    setReservedClientClasses(inf_request, ctx);
 
     // Create a Reply packet, with the same trans-id as the client's.
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, inf_request->getTransid()));
@@ -2907,11 +2917,24 @@ void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
                 .arg("get exception?");
         }
     }
+}
+
+void
+Dhcpv6Srv::setReservedClientClasses(const Pkt6Ptr& pkt,
+                                    const AllocEngine::ClientContext6& ctx) {
+    if (ctx.host_ && pkt) {
+        BOOST_FOREACH(const std::string& client_class,
+                      ctx.host_->getClientClasses6()) {
+            pkt->addClass(client_class);
+        }
+    }
 
+    const ClientClasses& classes = pkt->getClasses();
     if (!classes.empty()) {
+        std::string joined_classes = boost::algorithm::join(classes, ", ");
         LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_ASSIGNED)
             .arg(pkt->getLabel())
-            .arg(classes);
+            .arg(joined_classes);
     }
 }
 

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

@@ -629,6 +629,13 @@ protected:
     /// @param pkt packet to be classified
     void classifyPacket(const Pkt6Ptr& pkt);
 
+    /// @brief Assigns classes retrieved from host reservation database.
+    ///
+    /// @param pkt Pointer to the packet to which classes will be assigned.
+    /// @param ctx Reference to the client context.
+    void setReservedClientClasses(const Pkt6Ptr& pkt,
+                                  const AllocEngine::ClientContext6& ctx);
+
     /// @brief Attempts to get a MAC/hardware address using configred sources
     ///
     /// Tries to extract MAC/hardware address information from the packet

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

@@ -92,6 +92,7 @@ dhcp6_unittests_SOURCES += decline_unittest.cc
 dhcp6_unittests_SOURCES += dhcp6_message_test.cc dhcp6_message_test.h
 dhcp6_unittests_SOURCES += kea_controller_unittest.cc
 dhcp6_unittests_SOURCES += dhcp6to4_ipc_unittest.cc
+dhcp6_unittests_SOURCES += classify_unittests.cc
 
 nodist_dhcp6_unittests_SOURCES  = marker_file.h test_libraries.h
 

+ 749 - 0
src/bin/dhcp6/tests/classify_unittests.cc

@@ -0,0 +1,749 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcp/option_int.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/pkt6.h>
+#include <dhcp/tests/iface_mgr_test_config.h>
+#include <dhcp/opaque_data_tuple.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_vendor_class.h>
+#include <dhcp/option6_addrlst.h>
+#include <dhcp/tests/pkt_captures.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcp6/tests/dhcp6_test_utils.h>
+#include <dhcp6/tests/dhcp6_client.h>
+#include <asiolink/io_address.h>
+#include <boost/pointer_cast.hpp>
+#include <string>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// @brief Set of JSON configurations used by the classification unit tests.
+///
+/// - Configuration 0:
+///   - Specifies 3 classes: 'router', 'reserved-class1' and 'reserved-class2'.
+///   - 'router' class is assigned when the client sends option 1234 (string)
+///      equal to 'foo'.
+///   - The other two classes are reserved for the client having
+///     DUID '01:02:03:04'
+///   - Class 'router' includes option 'ipv6-forwarding'.
+///   - Class 'reserved-class1' includes option DNS servers.
+///   - Class 'reserved-class2' includes option NIS servers.
+///   - All three options are sent when client has reservations for the
+///     'reserved-class1', 'reserved-class2' and sends option 1234 with
+///     the 'foo' value.
+///   - There is one subnet specified 2001:db8:1::/48 with pool of
+///     IPv6 addresses.
+const char* CONFIGS[] = {
+    // Configuration 0
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"option-def\": [ "
+        "{"
+        "    \"name\": \"host-name\","
+        "    \"code\": 1234,"
+        "    \"type\": \"string\""
+        "},"
+        "{"
+        "    \"name\": \"ipv6-forwarding\","
+        "    \"code\": 2345,"
+        "    \"type\": \"boolean\""
+        "} ],"
+        "\"client-classes\": ["
+        "{"
+        "   \"name\": \"router\","
+        "   \"test\": \"option[host-name].text == 'foo'\","
+        "    \"option-data\": ["
+        "    {"
+        "        \"name\": \"ipv6-forwarding\", "
+        "        \"data\": \"true\""
+        "    } ]"
+        "},"
+        "{"
+        "   \"name\": \"reserved-class1\","
+        "   \"option-data\": ["
+        "   {"
+        "       \"name\": \"dns-servers\","
+        "       \"data\": \"2001:db8:1::50\""
+        "   }"
+        "   ]"
+        "},"
+        "{"
+        "   \"name\": \"reserved-class2\","
+        "   \"option-data\": ["
+        "   {"
+        "       \"name\": \"nis-servers\","
+        "       \"data\": \"2001:db8:1::100\""
+        "   }"
+        "   ]"
+        "}"
+        "],"
+        "\"subnet6\": [ "
+        "{   \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ], "
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"eth1\","
+        "    \"reservations\": ["
+        "    {"
+        "        \"duid\": \"01:02:03:04\","
+        "        \"client-classes\": [ \"reserved-class1\", \"reserved-class2\" ]"
+        "    } ]"
+        " } ],"
+        "\"valid-lifetime\": 4000 }"
+};
+
+/// @brief Test fixture class for testing client classification by the
+/// DHCPv6 server.
+///
+/// @todo There are numerous tests not using Dhcp6Client class. They should be
+/// migrated to use it one day.
+class ClassifyTest : public Dhcpv6SrvTest {
+public:
+    /// @brief Constructor.
+    ///
+    /// Sets up fake interfaces.
+    ClassifyTest()
+        : Dhcpv6SrvTest(),
+          iface_mgr_test_config_(true) {
+    }
+
+    /// @brief Verify values of options returned by the server when the server
+    /// uses configuration with index 0.
+    ///
+    /// @param config Reference to DHCP client's configuration received.
+    /// @param ip_forwarding Expected value of IP forwarding option. This option
+    /// is expected to always be present.
+    /// @param dns_servers String holding an address carried within DNS
+    /// servers option. If this value is empty, the option is expected to not
+    /// be included in the response.
+    /// @param nis_servers String holding an address carried within NIS
+    /// servers option. If this value is empty, the option is expected to not
+    /// be included in the response.
+    void verifyConfig0Options(const Dhcp6Client::Configuration& config,
+                              const uint8_t ip_forwarding = 1,
+                              const std::string& dns_servers = "",
+                              const std::string& nis_servers = "") {
+        // IP forwarding option should always exist.
+        OptionPtr ip_forwarding_opt = config.findOption(2345);
+        ASSERT_TRUE(ip_forwarding_opt);
+        // The option comprises 2 bytes of option code, 2 bytes of option length,
+        // and a single 1 byte value. This makes it 5 bytes of a total length.
+        ASSERT_EQ(5, ip_forwarding_opt->len());
+        ASSERT_EQ(static_cast<int>(ip_forwarding),
+                  static_cast<int>(ip_forwarding_opt->getUint8()));
+
+        // DNS servers.
+        Option6AddrLstPtr dns_servers_opt = boost::dynamic_pointer_cast<
+            Option6AddrLst>(config.findOption(D6O_NAME_SERVERS));
+        if (!dns_servers.empty()) {
+            ASSERT_TRUE(dns_servers_opt);
+            Option6AddrLst::AddressContainer addresses = dns_servers_opt->getAddresses();
+            // For simplicity, we expect only a single address.
+            ASSERT_EQ(1, addresses.size());
+            EXPECT_EQ(dns_servers, addresses[0].toText());
+
+        } else {
+            EXPECT_FALSE(dns_servers_opt);
+        }
+
+        // NIS servers.
+        Option6AddrLstPtr nis_servers_opt = boost::dynamic_pointer_cast<
+            Option6AddrLst>(config.findOption(D6O_NIS_SERVERS));
+        if (!nis_servers.empty()) {
+            ASSERT_TRUE(nis_servers_opt);
+            Option6AddrLst::AddressContainer addresses = nis_servers_opt->getAddresses();
+            // For simplicity, we expect only a single address.
+            ASSERT_EQ(1, addresses.size());
+            EXPECT_EQ(nis_servers, addresses[0].toText());
+
+        } else {
+            EXPECT_FALSE(nis_servers_opt);
+        }
+    }
+
+    /// @brief Interface Manager's fake configuration control.
+    IfaceMgrTestConfig iface_mgr_test_config_;
+};
+
+// Checks if DOCSIS client packets are classified properly
+TEST_F(ClassifyTest, docsisClientClassification) {
+
+    NakedDhcpv6Srv srv(0);
+
+    // Let's create a relayed SOLICIT. This particular relayed SOLICIT has
+    // vendor-class set to docsis3.0
+    Pkt6Ptr sol1;
+    ASSERT_NO_THROW(sol1 = PktCaptures::captureDocsisRelayedSolicit());
+    ASSERT_NO_THROW(sol1->unpack());
+
+    srv.classifyPacket(sol1);
+
+    // It should belong to docsis3.0 class. It should not belong to eRouter1.0
+    EXPECT_TRUE(sol1->inClass("VENDOR_CLASS_docsis3.0"));
+    EXPECT_FALSE(sol1->inClass("eRouter1.0"));
+
+    // Let's get a relayed SOLICIT. This particular relayed SOLICIT has
+    // vendor-class set to eRouter1.0
+    Pkt6Ptr sol2;
+    ASSERT_NO_THROW(sol2 = PktCaptures::captureeRouterRelayedSolicit());
+    ASSERT_NO_THROW(sol2->unpack());
+
+    srv.classifyPacket(sol2);
+
+    EXPECT_TRUE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
+    EXPECT_FALSE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
+}
+
+// Checks if client packets are classified properly using match expressions.
+// Note option names and definitions are used.
+TEST_F(ClassifyTest, 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.
+    std::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[host-name].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(ClassifyTest, 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.
+    std::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(ClassifyTest, 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.
+    std::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(ClassifyTest, 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.
+    std::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 ClassifyTest
+// .*Classification above.
+TEST_F(ClassifyTest, clientClassifySubnet) {
+
+    // This test configures 2 subnets. We actually only need the
+    // first one, but since there's still this ugly hack that picks
+    // the pool if there is only one, we must use more than one
+    // subnet. That ugly hack will be removed in #3242, currently
+    // under review.
+
+    // The second subnet does not play any role here. The client's
+    // IP address belongs to the first subnet, so only that first
+    // subnet is being tested.
+    std::string config = "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ "
+        " {  \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"client-class\": \"foo\" "
+        " }, "
+        " {  \"pools\": [ { \"pool\": \"2001:db8:2::/64\" } ],"
+        "    \"subnet\": \"2001:db8:2::/48\", "
+        "    \"client-class\": \"xyzzy\" "
+        " } "
+        "],"
+        "\"valid-lifetime\": 4000 }";
+
+    ASSERT_NO_THROW(configure(config));
+
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
+    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // This discover does not belong to foo class, so it will not
+    // be serviced
+    EXPECT_FALSE(srv_.selectSubnet(sol));
+
+    // Let's add the packet to bar class and try again.
+    sol->addClass("bar");
+
+    // Still not supported, because it belongs to wrong class.
+    EXPECT_FALSE(srv_.selectSubnet(sol));
+
+    // Let's add it to matching class.
+    sol->addClass("foo");
+
+    // This time it should work
+    EXPECT_TRUE(srv_.selectSubnet(sol));
+}
+
+// Tests whether a packet with custom vendor-class (not erouter or docsis)
+// is classified properly.
+TEST_F(ClassifyTest, vendorClientClassification2) {
+    NakedDhcpv6Srv srv(0);
+
+    // Let's create a SOLICIT.
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
+    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Now let's add a vendor-class with id=1234 and content "foo"
+    OptionVendorClassPtr vendor_class(new OptionVendorClass(Option::V6, 1234));
+    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
+    tuple = "foo";
+    vendor_class->addTuple(tuple);
+    sol->addOption(vendor_class);
+
+    // Now the server classifies the packet.
+    srv.classifyPacket(sol);
+
+    // The packet should now belong to VENDOR_CLASS_foo.
+    EXPECT_TRUE(sol->inClass(srv.VENDOR_CLASS_PREFIX + "foo"));
+
+    // It should not belong to "foo"
+    EXPECT_FALSE(sol->inClass("foo"));
+}
+
+// Checks if relay IP address specified in the relay-info structure can be
+// used together with client-classification.
+TEST_F(ClassifyTest, relayOverrideAndClientClass) {
+
+    // This test configures 2 subnets. They both are on the same link, so they
+    // have the same relay-ip address. Furthermore, the first subnet is
+    // reserved for clients that belong to class "foo".
+    std::string config = "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ "
+        " {  \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"client-class\": \"foo\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"2001:db8:3::1\""
+        "    }"
+        " }, "
+        " {  \"pools\": [ { \"pool\": \"2001:db8:2::/64\" } ],"
+        "    \"subnet\": \"2001:db8:2::/48\", "
+        "    \"relay\": { "
+        "        \"ip-address\": \"2001:db8:3::1\""
+        "    }"
+        " } "
+        "],"
+        "\"valid-lifetime\": 4000 }";
+
+    // Use this config to set up the server
+    ASSERT_NO_THROW(configure(config));
+
+    // Let's get the subnet configuration objects
+    const Subnet6Collection* subnets =
+        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
+    ASSERT_EQ(2, subnets->size());
+
+    // Let's get them for easy reference
+    Subnet6Ptr subnet1 = (*subnets)[0];
+    Subnet6Ptr subnet2 = (*subnets)[1];
+    ASSERT_TRUE(subnet1);
+    ASSERT_TRUE(subnet2);
+
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
+    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Now pretend the packet came via one relay.
+    Pkt6::RelayInfo relay;
+    relay.linkaddr_ = IOAddress("2001:db8:3::1");
+    relay.peeraddr_ = IOAddress("fe80::1");
+
+    sol->relay_info_.push_back(relay);
+
+    // This packet does not belong to class foo, so it should be rejected in
+    // subnet[0], even though the relay-ip matches. It should be accepted in
+    // subnet[1], because the subnet matches and there are no class
+    // requirements.
+    EXPECT_TRUE(subnet2 == srv_.selectSubnet(sol));
+
+    // Now let's add this packet to class foo and recheck. This time it should
+    // be accepted in the first subnet, because both class and relay-ip match.
+    sol->addClass("foo");
+    EXPECT_TRUE(subnet1 == srv_.selectSubnet(sol));
+}
+
+// This test checks that it is possible to specify static reservations for
+// client classes.
+TEST_F(ClassifyTest, clientClassesInHostReservations) {
+    Dhcp6Client client;
+    // Initially use a DUID for which there are no reservations. As a result,
+    // the client should be assigned a single class "router".
+    client.setDUID("01:02:03:05");
+    client.setInterface("eth1");
+    client.requestAddress();
+    // Request all options we may potentially get. Otherwise, the server will
+    // not return them, even when the client is assigned to the classes for
+    // which these options should be sent.
+    client.requestOption(2345);
+    client.requestOption(D6O_NAME_SERVERS);
+    client.requestOption(D6O_NIS_SERVERS);
+
+    ASSERT_NO_THROW(configure(CONFIGS[0], *client.getServer()));
+
+    // Adding this option to the client's message will cause the client to
+    // belong to the 'router' class.
+    OptionStringPtr hostname(new OptionString(Option::V6, 1234, "foo"));
+    client.addExtraOption(hostname);
+
+    // Send a message to the server.
+    ASSERT_NO_THROW(client.doSolicit(true));
+
+    // IP forwarding should be present, but DNS and NIS servers should not.
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_));
+
+    // Modify the DUID of our client to the one for which class reservations
+    // have been made.
+    client.setDUID("01:02:03:04");
+    ASSERT_NO_THROW(client.doSolicit(true));
+
+    // This time, the client should obtain options from all three classes.
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_, 1,
+                                                 "2001:db8:1::50",
+                                                 "2001:db8:1::100"));
+
+    // This should also work for Request case.
+    ASSERT_NO_THROW(client.doSARR());
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_, 1,
+                                                 "2001:db8:1::50",
+                                                 "2001:db8:1::100"));
+
+    // Renew case.
+    ASSERT_NO_THROW(client.doRenew());
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_, 1,
+                                                 "2001:db8:1::50",
+                                                 "2001:db8:1::100"));
+
+    // Rebind case.
+    ASSERT_NO_THROW(client.doRebind());
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_, 1,
+                                                 "2001:db8:1::50",
+                                                 "2001:db8:1::100"));
+
+    // Confirm case. This must be before Information-request because the
+    // client must have an address to confirm from one of the transactions
+    // involving address assignment, i.e. Request, Renew or Rebind.
+    ASSERT_NO_THROW(client.doConfirm());
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_, 1,
+                                                 "2001:db8:1::50",
+                                                 "2001:db8:1::100"));
+
+    // Information-request case.
+    ASSERT_NO_THROW(client.doInfRequest());
+    ASSERT_NO_FATAL_FAILURE(verifyConfig0Options(client.config_, 1,
+                                                 "2001:db8:1::50",
+                                                 "2001:db8:1::100"));
+}
+
+
+} // end of anonymous namespace

+ 1 - 1
src/bin/dhcp6/tests/dhcp6_client.cc

@@ -561,7 +561,7 @@ Dhcp6Client::doConfirm() {
     // Set the global status code to default: success and not received.
     config_.resetGlobalStatusCode();
     if (context_.response_) {
-        config_.resetGlobalStatusCode();
+        config_.options_.clear();
         applyRcvdConfiguration(context_.response_);
     }
 }

+ 1 - 0
src/bin/dhcp6/tests/dhcp6_client.h

@@ -110,6 +110,7 @@ public:
             leases_.clear();
             status_codes_.clear();
             resetGlobalStatusCode();
+            options_.clear();
         }
 
         /// @brief Clears global status code.

+ 0 - 495
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -1721,432 +1721,6 @@ TEST_F(Dhcpv6SrvTest, vendorOptionsDocsisDefinitions) {
     ASSERT_EQ(0, rcode_);
 }
 
-// Checks if DOCSIS client packets are classified properly
-TEST_F(Dhcpv6SrvTest, docsisClientClassification) {
-
-    NakedDhcpv6Srv srv(0);
-
-    // Let's create a relayed SOLICIT. This particular relayed SOLICIT has
-    // vendor-class set to docsis3.0
-    Pkt6Ptr sol1;
-    ASSERT_NO_THROW(sol1 = PktCaptures::captureDocsisRelayedSolicit());
-    ASSERT_NO_THROW(sol1->unpack());
-
-    srv.classifyPacket(sol1);
-
-    // It should belong to docsis3.0 class. It should not belong to eRouter1.0
-    EXPECT_TRUE(sol1->inClass("VENDOR_CLASS_docsis3.0"));
-    EXPECT_FALSE(sol1->inClass("eRouter1.0"));
-
-    // Let's get a relayed SOLICIT. This particular relayed SOLICIT has
-    // vendor-class set to eRouter1.0
-    Pkt6Ptr sol2;
-    ASSERT_NO_THROW(sol2 = PktCaptures::captureeRouterRelayedSolicit());
-    ASSERT_NO_THROW(sol2->unpack());
-
-    srv.classifyPacket(sol2);
-
-    EXPECT_TRUE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "eRouter1.0"));
-    EXPECT_FALSE(sol2->inClass(srv.VENDOR_CLASS_PREFIX + "docsis3.0"));
-}
-
-// Checks if client packets are classified properly using match expressions.
-// Note option names and definitions are used.
-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[host-name].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
-// .*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
-    // the pool if there is only one, we must use more than one
-    // subnet. That ugly hack will be removed in #3242, currently
-    // under review.
-
-    // The second subnet does not play any role here. The client's
-    // IP address belongs to the first subnet, so only that first
-    // subnet is being tested.
-    string config = "{ \"interfaces-config\": {"
-        "  \"interfaces\": [ \"*\" ]"
-        "},"
-        "\"preferred-lifetime\": 3000,"
-        "\"rebind-timer\": 2000, "
-        "\"renew-timer\": 1000, "
-        "\"subnet6\": [ "
-        " {  \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
-        "    \"subnet\": \"2001:db8:1::/48\", "
-        "    \"client-class\": \"foo\" "
-        " }, "
-        " {  \"pools\": [ { \"pool\": \"2001:db8:2::/64\" } ],"
-        "    \"subnet\": \"2001:db8:2::/48\", "
-        "    \"client-class\": \"xyzzy\" "
-        " } "
-        "],"
-        "\"valid-lifetime\": 4000 }";
-
-    ASSERT_NO_THROW(configure(config));
-
-    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
-    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
-    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
-    OptionPtr clientid = generateClientId();
-    sol->addOption(clientid);
-
-    // This discover does not belong to foo class, so it will not
-    // be serviced
-    EXPECT_FALSE(srv_.selectSubnet(sol));
-
-    // Let's add the packet to bar class and try again.
-    sol->addClass("bar");
-
-    // Still not supported, because it belongs to wrong class.
-    EXPECT_FALSE(srv_.selectSubnet(sol));
-
-    // Let's add it to matching class.
-    sol->addClass("foo");
-
-    // This time it should work
-    EXPECT_TRUE(srv_.selectSubnet(sol));
-}
-
-// Tests whether a packet with custom vendor-class (not erouter or docsis)
-// is classified properly.
-TEST_F(Dhcpv6SrvTest, vendorClientClassification2) {
-    NakedDhcpv6Srv srv(0);
-
-    // Let's create a SOLICIT.
-    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
-    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
-    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
-    OptionPtr clientid = generateClientId();
-    sol->addOption(clientid);
-
-    // Now let's add a vendor-class with id=1234 and content "foo"
-    OptionVendorClassPtr vendor_class(new OptionVendorClass(Option::V6, 1234));
-    OpaqueDataTuple tuple(OpaqueDataTuple::LENGTH_2_BYTES);
-    tuple = "foo";
-    vendor_class->addTuple(tuple);
-    sol->addOption(vendor_class);
-
-    // Now the server classifies the packet.
-    srv.classifyPacket(sol);
-
-    // The packet should now belong to VENDOR_CLASS_foo.
-    EXPECT_TRUE(sol->inClass(srv.VENDOR_CLASS_PREFIX + "foo"));
-
-    // It should not belong to "foo"
-    EXPECT_FALSE(sol->inClass("foo"));
-
-}
-
-
 // This test checks that the server will handle a Solicit with the Vendor Class
 // having a length of 4 (enterprise-id only).
 TEST_F(Dhcpv6SrvTest, cableLabsShortVendorClass) {
@@ -2251,75 +1825,6 @@ TEST_F(Dhcpv6SrvTest, relayOverride) {
     EXPECT_FALSE(srv_.selectSubnet(sol));
 }
 
-// Checks if relay IP address specified in the relay-info structure can be
-// used together with client-classification.
-TEST_F(Dhcpv6SrvTest, relayOverrideAndClientClass) {
-
-    // This test configures 2 subnets. They both are on the same link, so they
-    // have the same relay-ip address. Furthermore, the first subnet is
-    // reserved for clients that belong to class "foo".
-    string config = "{ \"interfaces-config\": {"
-        "  \"interfaces\": [ \"*\" ]"
-        "},"
-        "\"preferred-lifetime\": 3000,"
-        "\"rebind-timer\": 2000, "
-        "\"renew-timer\": 1000, "
-        "\"subnet6\": [ "
-        " {  \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
-        "    \"subnet\": \"2001:db8:1::/48\", "
-        "    \"client-class\": \"foo\", "
-        "    \"relay\": { "
-        "        \"ip-address\": \"2001:db8:3::1\""
-        "    }"
-        " }, "
-        " {  \"pools\": [ { \"pool\": \"2001:db8:2::/64\" } ],"
-        "    \"subnet\": \"2001:db8:2::/48\", "
-        "    \"relay\": { "
-        "        \"ip-address\": \"2001:db8:3::1\""
-        "    }"
-        " } "
-        "],"
-        "\"valid-lifetime\": 4000 }";
-
-    // Use this config to set up the server
-    ASSERT_NO_THROW(configure(config));
-
-    // Let's get the subnet configuration objects
-    const Subnet6Collection* subnets =
-        CfgMgr::instance().getCurrentCfg()->getCfgSubnets6()->getAll();
-    ASSERT_EQ(2, subnets->size());
-
-    // Let's get them for easy reference
-    Subnet6Ptr subnet1 = (*subnets)[0];
-    Subnet6Ptr subnet2 = (*subnets)[1];
-    ASSERT_TRUE(subnet1);
-    ASSERT_TRUE(subnet2);
-
-    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
-    sol->setRemoteAddr(IOAddress("2001:db8:1::3"));
-    sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
-    OptionPtr clientid = generateClientId();
-    sol->addOption(clientid);
-
-    // Now pretend the packet came via one relay.
-    Pkt6::RelayInfo relay;
-    relay.linkaddr_ = IOAddress("2001:db8:3::1");
-    relay.peeraddr_ = IOAddress("fe80::1");
-
-    sol->relay_info_.push_back(relay);
-
-    // This packet does not belong to class foo, so it should be rejected in
-    // subnet[0], even though the relay-ip matches. It should be accepted in
-    // subnet[1], because the subnet matches and there are no class
-    // requirements.
-    EXPECT_TRUE(subnet2 == srv_.selectSubnet(sol));
-
-    // Now let's add this packet to class foo and recheck. This time it should
-    // be accepted in the first subnet, because both class and relay-ip match.
-    sol->addClass("foo");
-    EXPECT_TRUE(subnet1 == srv_.selectSubnet(sol));
-}
-
 /// @brief Creates RSOO option with suboptions
 ///
 /// Creates Relay-Supplied Options option that includes nested options. The

+ 3 - 1
src/lib/dhcpsrv/cfg_hosts.cc

@@ -580,7 +580,9 @@ CfgHosts::add4(const HostPtr& host) {
         host->getServerHostname().empty() &&
         host->getBootFileName().empty() &&
         host->getCfgOption4()->empty() &&
-        host->getCfgOption6()->empty()) {
+        host->getCfgOption6()->empty() &&
+        host->getClientClasses4().empty() &&
+        host->getClientClasses6().empty()) {
         std::ostringstream s;
         if (hwaddr) {
             s << "for DUID: " << hwaddr->toText();

+ 1 - 0
src/lib/dhcpsrv/host.cc

@@ -347,6 +347,7 @@ Host::addClientClass4(const std::string& class_name) {
     addClientClassInternal(dhcp4_client_classes_, class_name);
 }
 
+
 void
 Host::addClientClass6(const std::string& class_name) {
     addClientClassInternal(dhcp6_client_classes_, class_name);

+ 28 - 12
src/lib/dhcpsrv/parsers/host_reservation_parser.cc

@@ -52,6 +52,7 @@ getSupportedParams4(const bool identifiers_only = false) {
         params_set.insert("next-server");
         params_set.insert("server-hostname");
         params_set.insert("boot-file-name");
+        params_set.insert("client-classes");
     }
     return (identifiers_only ? identifiers_set : params_set);
 }
@@ -83,6 +84,7 @@ getSupportedParams6(const bool identifiers_only = false) {
         params_set.insert("ip-addresses");
         params_set.insert("prefixes");
         params_set.insert("option-data");
+        params_set.insert("client-classes");
     }
     return (identifiers_only ? identifiers_set : params_set);
 }
@@ -102,10 +104,10 @@ HostReservationParser::build(isc::data::ConstElementPtr reservation_data) {
     std::string identifier_name;
     std::string hostname;
 
-    // Gather those parameters that are common for both IPv4 and IPv6
-    // reservations.
-    BOOST_FOREACH(ConfigPair element, reservation_data->mapValue()) {
-        try {
+    try {
+        // Gather those parameters that are common for both IPv4 and IPv6
+        // reservations.
+        BOOST_FOREACH(ConfigPair element, reservation_data->mapValue()) {
             // Check if we support this parameter.
             if (!isSupportedParameter(element.first)) {
                 isc_throw(DhcpConfigError, "unsupported configuration"
@@ -123,15 +125,10 @@ HostReservationParser::build(isc::data::ConstElementPtr reservation_data) {
 
             } else if (element.first == "hostname") {
                 hostname = element.second->stringValue();
+
             }
-        } catch (const std::exception& ex) {
-            // Append line number where the error occurred.
-            isc_throw(DhcpConfigError, ex.what() << " ("
-                      << element.second->getPosition() << ")");
         }
-    }
 
-    try {
         // Host identifier is a must.
         if (identifier_name.empty()) {
             // If there is no identifier specified, we have to display an
@@ -210,19 +207,25 @@ HostReservationParser4::build(isc::data::ConstElementPtr reservation_data) {
                     host_->setIPv4Reservation(IOAddress(element.second->
                                                         stringValue()));
                 } else if (element.first == "next-server") {
-                host_->setNextServer(IOAddress(element.second->stringValue()));
+                    host_->setNextServer(IOAddress(element.second->stringValue()));
 
                 } else if (element.first == "server-hostname") {
                     host_->setServerHostname(element.second->stringValue());
 
                 } else if (element.first == "boot-file-name") {
                     host_->setBootFileName(element.second->stringValue());
+
+                } else if (element.first == "client-classes") {
+                    BOOST_FOREACH(ConstElementPtr class_element,
+                                  element.second->listValue()) {
+                        host_->addClientClass4(class_element->stringValue());
+                    }
                 }
 
             } catch (const std::exception& ex) {
                 // Append line number where the error occurred.
                 isc_throw(DhcpConfigError, ex.what() << " ("
-                          << reservation_data->getPosition() << ")");
+                          << element.second->getPosition() << ")");
             }
         }
     }
@@ -318,6 +321,19 @@ HostReservationParser6::build(isc::data::ConstElementPtr reservation_data) {
                               << prefix_element->getPosition() << ")");
                 }
             }
+
+
+        } else if (element.first == "client-classes") {
+            try {
+                BOOST_FOREACH(ConstElementPtr class_element,
+                              element.second->listValue()) {
+                    host_->addClientClass6(class_element->stringValue());
+                }
+            } catch (const std::exception& ex) {
+                // Append line number where the error occurred.
+                isc_throw(DhcpConfigError, ex.what() << " ("
+                          << element.second->getPosition() << ")");
+            }
         }
     }
 

+ 46 - 0
src/lib/dhcpsrv/tests/host_reservation_parser_unittest.cc

@@ -300,6 +300,29 @@ TEST_F(HostReservationParserTest, dhcp4NoHostname) {
     EXPECT_TRUE(hosts[0]->getHostname().empty());
 }
 
+// This test verifies that it is possible to specify DHCPv4 client classes
+// within the host reservation.
+TEST_F(HostReservationParserTest, dhcp4ClientClasses) {
+    std::string config = "{ \"hw-address\": \"01:02:03:04:05:06\","
+        "\"client-classes\": [ \"foo\", \"bar\" ] }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser4 parser(SubnetID(10));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(hwaddr_));
+
+    ASSERT_EQ(1, hosts.size());
+
+    const ClientClasses& classes = hosts[0]->getClientClasses4();
+    ASSERT_EQ(2, classes.size());
+    EXPECT_EQ(1, classes.count("foo"));
+    EXPECT_EQ(1, classes.count("bar"));
+}
+
 // This test verifies that the parser can parse reservation entry
 // containing next-server, server-hostname and boot-file-name values for
 // DHCPv4 message fields.
@@ -627,6 +650,29 @@ TEST_F(HostReservationParserTest, dhcp6NoHostname) {
     ASSERT_EQ(0, std::distance(prefixes.first, prefixes.second));
 }
 
+// This test verifies that it is possible to specify DHCPv4 client classes
+// within the host reservation.
+TEST_F(HostReservationParserTest, dhcp6ClientClasses) {
+    std::string config = "{ \"duid\": \"01:02:03:04:05:06:07:08:09:0A\","
+        "\"client-classes\": [ \"foo\", \"bar\" ] }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    HostReservationParser6 parser(SubnetID(10));
+    ASSERT_NO_THROW(parser.build(config_element));
+
+    CfgHostsPtr cfg_hosts = CfgMgr::instance().getStagingCfg()->getCfgHosts();
+    HostCollection hosts;
+    ASSERT_NO_THROW(hosts = cfg_hosts->getAll(Host::IDENT_DUID,
+                                              &duid_->getDuid()[0],
+                                              duid_->getDuid().size()));
+    ASSERT_EQ(1, hosts.size());
+
+    const ClientClasses& classes = hosts[0]->getClientClasses6();
+    ASSERT_EQ(2, classes.size());
+    EXPECT_EQ(1, classes.count("foo"));
+    EXPECT_EQ(1, classes.count("bar"));
+}
 
 // This test verifies that the configuration parser throws an exception
 // when IPv4 address is specified for IPv6 reservation.