]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1155] Backported #1139 from master
authorMarcin Siodelski <marcin@isc.org>
Thu, 19 Mar 2020 12:29:45 +0000 (13:29 +0100)
committerMarcin Siodelski <marcin@isc.org>
Thu, 19 Mar 2020 13:46:45 +0000 (14:46 +0100)
This is an enhancement to client classification in DHCPv4
and DHCPv6 servers. The client classes spefified in the
host database are taken into account in evaluation of the
client classes specified within the configuration files.
This works both for the global reservations and for the
non-global reservations when the selected subnet belongs
to a shared network. It can be used to influence subnet
selection within a shared network using host reservations
or pool selection within a subnet that doesn't necessarily
belong to a shared network.

14 files changed:
ChangeLog
doc/sphinx/arm/dhcp4-srv.rst
doc/sphinx/arm/dhcp6-srv.rst
src/bin/dhcp4/dhcp4_srv.cc
src/bin/dhcp4/dhcp4_srv.h
src/bin/dhcp4/tests/host_unittest.cc
src/bin/dhcp6/dhcp6_srv.cc
src/bin/dhcp6/dhcp6_srv.h
src/bin/dhcp6/tests/host_unittest.cc
src/lib/dhcp/classify.cc
src/lib/dhcp/classify.h
src/lib/dhcp/tests/classify_unittest.cc
src/lib/dhcpsrv/alloc_engine.cc
src/lib/dhcpsrv/alloc_engine.h

index e29021bd9be68850310a60a6bd4afce88072b027..1bdae674d1f3519b7e85a453a8219e97f25afd93 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+1666.  [func]          marcin
+       Client classes specified within host reservations can be used
+       to influence subnet selection within a shared network and pool
+       selection within a subnet.
+       (Gitlab #1155)
+
 Kea 1.6.2 released on Feb 19, 2020
 
 1665.  [bug]           tmark
index 65e2ccadc1d1414db373690d2a7a5d6995d28d61..b5cb846c2438e4d9fa855c52e6f10fcdc01b0af0 100644 (file)
@@ -4018,11 +4018,11 @@ Reserving Client Classes in DHCPv4
 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 the static assignment of classes to clients.
-The definitions of these classes are placed in the Kea configuration.
-The following configuration snippet shows how to specify that a client
-belongs to classes ``reserved-class1`` and ``reserved-class2``. Those
-classes are associated with specific options that are sent to the clients
-which belong to them.
+The definitions of these classes are placed in the Kea configuration or
+a database. The following configuration snippet shows how to specify that
+a client belongs to classes ``reserved-class1`` and ``reserved-class2``. Those
+classes are associated with specific options sent to the clients which belong
+to them.
 
 ::
 
@@ -4061,30 +4061,52 @@ which belong to them.
        } ]
    }
 
-Static class assignments, as shown above, can be used in conjunction
-with classification, using expressions. The "KNOWN" or "UNKNOWN" built-in
-class is added to the packet and any class depending on it (directly or
-indirectly) and not only-if-required is evaluated.
+In some cases the host reservations can be used in conjuction with client
+classes specified within the Kea configuration. In particular, when a
+host reservation exists for a client within a given subnet, the "KNOWN"
+built-in class is assigned to the client. Conversely, when there is no
+static assignment for the client, the "UNKNOWN" class is assigned to the
+client. Class expressions within the Kea configuration file can
+refer to "KNOWN" or "UNKNOWN" classes using using the "member" operator.
+For example:
 
-.. note::
+::
+
+    {
+        "client-classes": [
+            {
+                "name": "dependent-class",
+                "test": "member('KNOWN')",
+                "only-if-required": true
+            }
+        ]
+    }
 
-   To force the evaluation of a class expression after the
-   host reservation lookup, for instance because of a dependency on
-   "reserved-class1" from the previous example, add a
-   "member('KNOWN')" statement in the expression.
+Note that the ``only-if-required`` parameter is needed here to force
+evaluation of the class after the lease has been allocated and thus the
+reserved class has been also assigned.
+
+.. note::
+   Be aware that the classes specified in non global host reservations
+   are assigned to the processed packet after all classes with the
+   ``only-if-required`` parameter set to ``false`` have been evaluated.
+   This has an implication that these classes must not depend on the
+   statically assigned classes from the host reservations. If there
+   is a need to create such dependency, the ``only-if-required`` must
+   be set to ``true`` for the dependent classes. Such classes are
+   evaluated after the static classes have been assigned to the packet.
+   This, however, imposes additional configuration overhead, because
+   all classes marked as ``only-if-required`` must be listed in the
+   ``require-client-classes`` list for every subnet where they are used.
 
 .. note::
-   Beware that the reserved classes are assigned to the processed
-   packet after all classes with the ``only-if-required`` parameter
-   set to ``false`` have been evaluated. This has an implication that
-   these classes must not depend on the statically assigned classes
-   from the host reservations. If there is a need to create such
-   dependency, the ``only-if-required`` must be set to ``true`` for
-   the dependent classes. Such classes are evaluated after the static
-   classes have been assigned to the packet. This, however, imposes
-   additional configuration overhead, because all classes marked as
-   ``only-if-required`` must be listed in the ``require-client-classes``
-   list for every subnet where they are used.
+   Client classes specified within the Kea configuration file may
+   depend on the classes specified within the global host reservations.
+   In such case the ``only-if-required`` parameter is not needed.
+   Refer to the :ref:`pool-selection-with-class-reservations4` and
+   :ref:`subnet-selection-with-class-reservations4`
+   for the specific use cases.
+
 
 .. _reservations4-mysql-pgsql-cql:
 
@@ -4315,6 +4337,140 @@ When using database backends, the global host reservations are
 distinguished from regular reservations by using a subnet-id value of
 zero.
 
+.. _pool-selection-with-class-reservations4:
+
+Pool Selection with Client Class Reservations
+---------------------------------------------
+
+Client classes can be specified both in the Kea configuration file and/or
+host reservations. The classes specified in the Kea configuration file are
+evaluated immediately after receiving the DHCP packet and therefore can be
+used to influence subnet selection using the ``client-class`` parameter
+specified in the subnet scope. The classes specified within the host
+reservations are fetched and assigned to the packet after the server has
+already selected a subnet for the client. This means that the client
+class specified within a host reservation cannot be used to influence
+subnet assignment for this client, unless the subnet belongs to a
+shared network. If the subnet belongs to a shared network, the server may
+dynamically change the subnet assignment while trying to allocate a lease.
+If the subnet does not belong to a shared network, once selected, the subnet
+is not changed.
+
+If the subnet does not belong to a shared network, it is possible to
+use host reservation based client classification to select an address pool
+within the subnet as follows:
+
+::
+
+    "Dhcp4": {
+        "client-classes": [
+            {
+                "name": "reserved_class"
+            },
+            {
+                "name": "unreserved_class",
+                "test": "not member('reserved_class')"
+            }
+        ],
+        "subnet4": [
+            {
+                "subnet": "192.0.2.0/24",
+                "reservations": [{"
+                    "hw-address": "aa:bb:cc:dd:ee:fe",
+                    "client-classes": [ "reserved_class" ]
+                 }],
+                "pools": [
+                    {
+                        "pool": "192.0.2.10-192.0.2.20",
+                        "client-class": "reserved_class"
+                    },
+                    {
+                        "pool": "192.0.2.30-192.0.2.40",
+                        "client-class": "unreserved_class"
+                    }
+                ]
+            }
+        ]
+    }
+
+The ``reserved_class`` is declared without the ``test`` parameter because
+it may be only assigned to the client via host reservation mechanism. The
+second class, ``unreserved_class``, is assigned to the clients which do not
+belong to the ``reserved_class``.  The first pool within the subnet is only
+used for the clients having a reservation for the ``reserved_class``. The
+second pool is used for the clients not having such reservation. The
+configuration snippet includes one host reservation which causes the client
+having the MAC address of aa:bb:cc:dd:ee:fe to be assigned to the
+``reserved_class``. Thus, this client will be given an IP address from the
+first address pool.
+
+.. _subnet-selection-with-class-reservations4:
+
+Subnet Selection with Client Class Reservations
+-----------------------------------------------
+
+There is one specific use case when subnet selection may be influenced by
+client classes specified within host reservations. This is the case when the
+client belongs to a shared network. In such case it is possible to use
+classification to select a subnet within this shared network. Consider the
+following example:
+
+::
+
+    "Dhcp4": {
+        "client-classes": [
+            {
+                "name": "reserved_class"
+            },
+            {
+                "name: "unreserved_class",
+                "test": "not member('reserved_class")
+            }
+        ],
+        "reservations": [{"
+            "hw-address": "aa:bb:cc:dd:ee:fe",
+            "client-classes": [ "reserved_class" ]
+        }],
+        "reservation-mode": "global",
+        "shared-networks": [{
+            "subnet4": [
+                {
+                    "subnet": "192.0.2.0/24",
+                    "pools": [
+                        {
+                            "pool": "192.0.2.10-192.0.2.20",
+                            "client-class": "reserved_class"
+                        }
+                    ]
+                },
+                {
+                    "subnet": "192.0.3.0/24",
+                    "pools": [
+                        {
+                            "pool": "192.0.3.10-192.0.3.20",
+                            "client-class": "unreserved_class"
+                        }
+                    ]
+                }
+            ]
+        }]
+    }
+
+This is similar to the example described in the
+:ref:`pool-selection-with-class-reservations4`. This time, however, there
+are two subnets, each of them having a pool associated with a different
+class. The clients which don't have a reservation for the ``reserved_class``
+will be assigned an address from the subnet 192.0.3.0/24. Clients having
+a reservation for the ``reserved_class`` will be assigned an address from
+the subnet 192.0.2.0/24. The subnets must belong to the same shared network.
+In addition, the reservation for the client class must be specified at the
+global scope (global reservation) and the ``reservation-mode`` must be
+set to ``global``.
+
+In the example above the ``client-class`` could also be specified at the
+subnet level rather than pool level yielding the same effect.
+
+
 .. _shared-network4:
 
 Shared Networks in DHCPv4
index 5a4046212f596190b9c6d78c2be1dc437f8e4ea4..edf3497ce06909ac82d66d25ec7e7b3d10cfed6d 100644 (file)
@@ -3483,11 +3483,11 @@ Reserving Client Classes in DHCPv6
 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 the static assignment of classes to clients.
-The definitions of these classes are placed in the Kea configuration.
-The following configuration snippet shows how to specify that the client
-belongs to classes ``reserved-class1`` and ``reserved-class2``. Those
-classes are associated with specific options that are sent to the clients
-which belong to them.
+The definitions of these classes are placed in the Kea configuration or
+a database. The following configuration snippet shows how to specify that
+a client belongs to classes ``reserved-class1`` and ``reserved-class2``. Those
+classes are associated with specific options sent to the clients which belong
+to them.
 
 ::
 
@@ -3525,30 +3525,51 @@ which belong to them.
         } ]
     }
 
-Static class assignments, as shown above, can be used in conjunction
-with classification, using expressions. The "KNOWN" or "UNKNOWN" built-in
-class is added to the packet and any class depending on it (directly or
-indirectly) and not only-if-required is evaluated.
+In some cases the host reservations can be used in conjuction with client
+classes specified within the Kea configuration. In particular, when a
+host reservation exists for a client within a given subnet, the "KNOWN"
+built-in class is assigned to the client. Conversely, when there is no
+static assignment for the client, the "UNKNOWN" class is assigned to the
+client. Class expressions within the Kea configuration file can
+refer to "KNOWN" or "UNKNOWN" classes using using the "member" operator.
+For example:
 
-.. note::
+::
+
+    {
+        "client-classes": [
+            {
+                "name": "dependent-class",
+                "test": "member('KNOWN')",
+                "only-if-required": true
+            }
+        ]
+    }
 
-   To force the evaluation of a class expression after the
-   host reservation lookup, for instance because of a dependency on
-   "reserved-class1" from the previous example, add a
-   "member('KNOWN')" statement in the expression.
+Note that the ``only-if-required`` parameter is needed here to force
+evaluation of the class after the lease has been allocated and thus the
+reserved class has been also assigned.
 
 .. note::
-   Beware that the reserved classes are assigned to the processed
-   packet after all classes with the ``only-if-required`` parameter
-   set to ``false`` have been evaluated. This has an implication that
-   these classes must not depend on the statically assigned classes
-   from the host reservations. If there is a need to create such
-   dependency, the ``only-if-required`` must be set to ``true`` for
-   the dependent classes. Such classes are evaluated after the static
-   classes have been assigned to the packet. This, however, imposes
-   additional configuration overhead, because all classes marked as
-   ``only-if-required`` must be listed in the ``require-client-classes``
-   list for every subnet where they are used.
+   Be aware that the classes specified in non global host reservations
+   are assigned to the processed packet after all classes with the
+   ``only-if-required`` parameter set to ``false`` have been evaluated.
+   This has an implication that these classes must not depend on the
+   statically assigned classes from the host reservations. If there
+   is a need to create such dependency, the ``only-if-required`` must
+   be set to ``true`` for the dependent classes. Such classes are
+   evaluated after the static classes have been assigned to the packet.
+   This, however, imposes additional configuration overhead, because
+   all classes marked as ``only-if-required`` must be listed in the
+   ``require-client-classes`` list for every subnet where they are used.
+
+.. note::
+   Client classes specified within the Kea configuration file may
+   depend on the classes specified within the global host reservations.
+   In such case the ``only-if-required`` parameter is not needed.
+   Refer to the :ref:`pool-selection-with-class-reservations6` and
+   :ref:`subnet-selection-with-class-reservations6`
+   for the specific use cases.
 
 .. _reservations6-mysql-pgsql-cql:
 
@@ -3777,6 +3798,140 @@ When using database backends, the global host reservations are
 distinguished from regular reservations by using subnet-id value of
 zero.
 
+.. _pool-selection-with-class-reservations6:
+
+Pool Selection with Client Class Reservations
+---------------------------------------------
+
+Client classes can be specified both in the Kea configuration file and/or
+host reservations. The classes specified in the Kea configuration file are
+evaluated immediately after receiving the DHCP packet and therefore can be
+used to influence subnet selection using the ``client-class`` parameter
+specified in the subnet scope. The classes specified within the host
+reservations are fetched and assigned to the packet after the server has
+already selected a subnet for the client. This means that the client
+class specified within a host reservation cannot be used to influence
+subnet assignment for this client, unless the subnet belongs to a
+shared network. If the subnet belongs to a shared network, the server may
+dynamically change the subnet assignment while trying to allocate a lease.
+If the subnet does not belong to a shared network, once selected, the subnet
+is not changed.
+
+If the subnet does not belong to a shared network, it is possible to
+use host reservation based client classification to select an address pool
+within the subnet as follows:
+
+::
+
+    "Dhcp6": {
+        "client-classes": [
+            {
+                "name": "reserved_class"
+            },
+            {
+                "name": "unreserved_class",
+                "test": "not member('reserved_class')"
+            }
+        ],
+        "subnet6": [
+            {
+                "subnet": "2001:db8:1::/64",
+                "reservations": [{"
+                    "hw-address": "aa:bb:cc:dd:ee:fe",
+                    "client-classes": [ "reserved_class" ]
+                 }],
+                "pools": [
+                    {
+                        "pool": "2001:db8:1::10-2001:db8:1::20",
+                        "client-class": "reserved_class"
+                    },
+                    {
+                        "pool": "2001:db8:1::30-2001:db8:1::40",
+                        "client-class": "unreserved_class"
+                    }
+                ]
+            }
+        ]
+    }
+
+The ``reserved_class`` is declared without the ``test`` parameter because
+it may be only assigned to the client via host reservation mechanism. The
+second class, ``unreserved_class``, is assigned to the clients which do not
+belong to the ``reserved_class``.  The first pool within the subnet is only
+used for the clients having a reservation for the ``reserved_class``. The
+second pool is used for the clients not having such reservation. The
+configuration snippet includes one host reservation which causes the client
+having the MAC address of aa:bb:cc:dd:ee:fe to be assigned to the
+``reserved_class``. Thus, this client will be given an IP address from the
+first address pool.
+
+.. _subnet-selection-with-class-reservations6:
+
+Subnet Selection with Client Class Reservations
+-----------------------------------------------
+
+There is one specific use case when subnet selection may be influenced by
+client classes specified within host reservations. This is the case when the
+client belongs to a shared network. In such case it is possible to use
+classification to select a subnet within this shared network. Consider the
+following example:
+
+::
+
+    "Dhcp6": {
+        "client-classes": [
+            {
+                "name": "reserved_class"
+            },
+            {
+                "name: "unreserved_class",
+                "test": "not member('reserved_class")
+            }
+        ],
+        "reservations": [{"
+            "hw-address": "aa:bb:cc:dd:ee:fe",
+            "client-classes": [ "reserved_class" ]
+        }],
+        "reservation-mode": "global",
+        "shared-networks": [{
+            "subnet6": [
+                {
+                    "subnet": "2001:db8:1::/64",
+                    "pools": [
+                        {
+                            "pool": "2001:db8:1::10-2001:db8:1::20",
+                            "client-class": "reserved_class"
+                        }
+                    ]
+                },
+                {
+                    "subnet": "2001:db8:2::/64",
+                    "pools": [
+                        {
+                            "pool": "2001:db8:2::10-2001:db8:2::20",
+                            "client-class": "unreserved_class"
+                        }
+                    ]
+                }
+            ]
+        }]
+    }
+
+This is similar to the example described in the
+:ref:`pool-selection-with-class-reservations6`. This time, however, there
+are two subnets, each of them having a pool associated with a different
+class. The clients which don't have a reservation for the ``reserved_class``
+will be assigned an address from the subnet 2001:db8:2::/64. Clients having
+a reservation for the ``reserved_class`` will be assigned an address from
+the subnet 2001:db8:1::/64. The subnets must belong to the same shared network.
+In addition, the reservation for the client class must be specified at the
+global scope (global reservation) and the ``reservation-mode`` must be
+set to ``global``.
+
+In the example above the ``client-class`` could also be specified at the
+subnet level rather than pool level yielding the same effect.
+
+
 .. _shared-network6:
 
 Shared Networks in DHCPv6
index fab7cd44058938491677866ff91049c7efb594e9..a203bb3774fe02fe017915183e373b68443456c2 100644 (file)
@@ -148,6 +148,7 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine,
     // If subnet found, retrieve client identifier which will be needed
     // for allocations and search for reservations associated with a
     // subnet/shared network.
+    SharedNetwork4Ptr sn;
     if (subnet) {
         OptionPtr opt_clientid = query->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
         if (opt_clientid) {
@@ -162,7 +163,44 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine,
 
             // Check for static reservations.
             alloc_engine->findReservation(*context_);
+
+            // Get shared network to see if it is set for a subnet.
+            subnet->getSharedNetwork(sn);
+        }
+    }
+
+    // Global host reservations are independent of a selected subnet. If the
+    // global reservations contain client classes we should use them in case
+    // they are meant to affect pool selection. Also, if the subnet does not
+    // belong to a shared network we can use the reserved client classes
+    // because there is no way our subnet could change. Such classes may
+    // affect selection of a pool within the selected subnet.
+    auto global_host = context_->globalHost();
+    auto current_host = context_->currentHost();
+    if ((global_host && !global_host->getClientClasses4().empty()) ||
+        (!sn && current_host && !current_host->getClientClasses4().empty())) {
+        // We have already evaluated client classes and some of them may
+        // be in conflict with the reserved classes. Suppose there are
+        // two classes defined in the server configuration: first_class
+        // and second_class and the test for the second_class it looks
+        // like this: "not member('first_class')". If the first_class
+        // initially evaluates to false, the second_class evaluates to
+        // true. If the first_class is now set within the hosts reservations
+        // and we don't remove the previously evaluated second_class we'd
+        // end up with both first_class and second_class evaluated to
+        // true. In order to avoid that, we have to remove the classes
+        // evaluated in the first pass and evaluate them again. As
+        // a result, the first_class set via the host reservation will
+        // replace the second_class because the second_class will this
+        // time evaluate to false as desired.
+        const ClientClassDictionaryPtr& dict =
+            CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+        const ClientClassDefListPtr& defs_ptr = dict->getClasses();
+        for (auto def : *defs_ptr) {
+            context_->query_->classes_.erase(def->getName());
         }
+        setReservedClientClasses(context_);
+        evaluateClasses(context_->query_, false);
     }
 
     // Set KNOWN builtin class if something was found, UNKNOWN if not.
@@ -179,7 +217,7 @@ Dhcpv4Exchange::Dhcpv4Exchange(const AllocEnginePtr& alloc_engine,
     }
 
     // Perform second pass of classification.
-    Dhcpv4Srv::evaluateClasses(query, true);
+    evaluateClasses(query, true);
 
     const ClientClasses& classes = query_->getClasses();
     if (!classes.empty()) {
@@ -411,12 +449,23 @@ Dhcpv4Exchange::setHostIdentifiers() {
 }
 
 void
-Dhcpv4Exchange::setReservedClientClasses() {
-    if (context_->currentHost() && query_) {
-        const ClientClasses& classes = context_->currentHost()->getClientClasses4();
+Dhcpv4Exchange::setReservedClientClasses(AllocEngine::ClientContext4Ptr context) {
+    if (context->currentHost() && context->query_) {
+        const ClientClasses& classes = context->currentHost()->getClientClasses4();
         for (ClientClasses::const_iterator cclass = classes.cbegin();
              cclass != classes.cend(); ++cclass) {
-            query_->addClass(*cclass);
+            context->query_->addClass(*cclass);
+        }
+    }
+}
+
+void
+Dhcpv4Exchange::conditionallySetReservedClientClasses() {
+    if (context_->subnet_) {
+        SharedNetwork4Ptr shared_network;
+        context_->subnet_->getSharedNetwork(shared_network);
+        if (shared_network && !context_->globalHost()) {
+            setReservedClientClasses(context_);
         }
     }
 }
@@ -444,6 +493,78 @@ Dhcpv4Exchange::setReservedMessageFields() {
     }
 }
 
+void Dhcpv4Exchange::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));
+
+    if (!vendor_class) {
+        return;
+    }
+
+    pkt->addClass(Dhcpv4Srv::VENDOR_CLASS_PREFIX + vendor_class->getValue());
+}
+
+void Dhcpv4Exchange::classifyPacket(const Pkt4Ptr& pkt) {
+    // All packets belongs to ALL.
+    pkt->addClass("ALL");
+
+    // First: built-in vendor class processing.
+    classifyByVendor(pkt);
+
+    // Run match expressions on classes not depending on KNOWN/UNKNOWN.
+    evaluateClasses(pkt, false);
+}
+
+void Dhcpv4Exchange::evaluateClasses(const Pkt4Ptr& pkt, bool depend_on_known) {
+    // Note getClientClassDictionary() cannot be null
+    const ClientClassDictionaryPtr& dict =
+        CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+    const ClientClassDefListPtr& defs_ptr = dict->getClasses();
+    for (ClientClassDefList::const_iterator it = defs_ptr->cbegin();
+         it != defs_ptr->cend(); ++it) {
+        // Note second cannot be null
+        const ExpressionPtr& expr_ptr = (*it)->getMatchExpr();
+        // Nothing to do without an expression to evaluate
+        if (!expr_ptr) {
+            continue;
+        }
+        // Not the right time if only when required
+        if ((*it)->getRequired()) {
+            continue;
+        }
+        // Not the right pass.
+        if ((*it)->getDependOnKnown() != depend_on_known) {
+            continue;
+        }
+        // Evaluate the expression which can return false (no match),
+        // true (match) or raise an exception (error)
+        try {
+            bool status = evaluateBool(*expr_ptr, *pkt);
+            if (status) {
+                LOG_INFO(options4_logger, EVAL_RESULT)
+                    .arg((*it)->getName())
+                    .arg(status);
+                // Matching: add the class
+                pkt->addClass((*it)->getName());
+            } else {
+                LOG_DEBUG(options4_logger, DBG_DHCP4_DETAIL, EVAL_RESULT)
+                    .arg((*it)->getName())
+                    .arg(status);
+            }
+        } catch (const Exception& ex) {
+            LOG_ERROR(options4_logger, EVAL_RESULT)
+                .arg((*it)->getName())
+                .arg(ex.what());
+        } catch (...) {
+            LOG_ERROR(options4_logger, EVAL_RESULT)
+                .arg((*it)->getName())
+                .arg("get exception?");
+        }
+    }
+}
+
+
 const std::string Dhcpv4Srv::VENDOR_CLASS_PREFIX("VENDOR_CLASS_");
 
 Dhcpv4Srv::Dhcpv4Srv(uint16_t server_port, uint16_t client_port,
@@ -2657,8 +2778,10 @@ Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
 
     // Adding any other options makes sense only when we got the lease.
     if (!ex.getResponse()->getYiaddr().isV4Zero()) {
-        // Assign reserved classes.
-        ex.setReservedClientClasses();
+        // If this is global reservation or the subnet doesn't belong to a shared
+        // network we have already fetched it and evaluated the classes.
+        ex.conditionallySetReservedClientClasses();
+
         // Required classification
         requiredClassify(ex);
 
@@ -2720,8 +2843,10 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request, AllocEngine::ClientContext4Ptr& cont
 
     // Adding any other options makes sense only when we got the lease.
     if (!ex.getResponse()->getYiaddr().isV4Zero()) {
-        // Assign reserved classes.
-        ex.setReservedClientClasses();
+        // If this is global reservation or the subnet doesn't belong to a shared
+        // network we have already fetched it and evaluated the classes.
+        ex.conditionallySetReservedClientClasses();
+
         // Required classification
         requiredClassify(ex);
 
@@ -3031,7 +3156,10 @@ Dhcpv4Srv::processInform(Pkt4Ptr& inform) {
 
     Pkt4Ptr ack = ex.getResponse();
 
-    ex.setReservedClientClasses();
+    // If this is global reservation or the subnet doesn't belong to a shared
+    // network we have already fetched it and evaluated the classes.
+    ex.conditionallySetReservedClientClasses();
+
     requiredClassify(ex);
 
     buildCfgOptionList(ex);
@@ -3326,75 +3454,8 @@ Dhcpv4Srv::sanityCheck(const Pkt4Ptr& query, RequirementLevel serverid) {
     }
 }
 
-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));
-
-    if (!vendor_class) {
-        return;
-    }
-
-    pkt->addClass(VENDOR_CLASS_PREFIX + vendor_class->getValue());
-}
-
 void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
-    // All packets belongs to ALL.
-    pkt->addClass("ALL");
-
-    // First: built-in vendor class processing.
-    classifyByVendor(pkt);
-
-    // Run match expressions on classes not depending on KNOWN/UNKNOWN.
-    evaluateClasses(pkt, false);
-}
-
-void Dhcpv4Srv::evaluateClasses(const Pkt4Ptr& pkt, bool depend_on_known) {
-    // Note getClientClassDictionary() cannot be null
-    const ClientClassDictionaryPtr& dict =
-        CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
-    const ClientClassDefListPtr& defs_ptr = dict->getClasses();
-    for (ClientClassDefList::const_iterator it = defs_ptr->cbegin();
-         it != defs_ptr->cend(); ++it) {
-        // Note second cannot be null
-        const ExpressionPtr& expr_ptr = (*it)->getMatchExpr();
-        // Nothing to do without an expression to evaluate
-        if (!expr_ptr) {
-            continue;
-        }
-        // Not the right time if only when required
-        if ((*it)->getRequired()) {
-            continue;
-        }
-        // Not the right pass.
-        if ((*it)->getDependOnKnown() != depend_on_known) {
-            continue;
-        }
-        // Evaluate the expression which can return false (no match),
-        // true (match) or raise an exception (error)
-        try {
-            bool status = evaluateBool(*expr_ptr, *pkt);
-            if (status) {
-                LOG_INFO(options4_logger, EVAL_RESULT)
-                    .arg((*it)->getName())
-                    .arg(status);
-                // Matching: add the class
-                pkt->addClass((*it)->getName());
-            } else {
-                LOG_DEBUG(options4_logger, DBG_DHCP4_DETAIL, EVAL_RESULT)
-                    .arg((*it)->getName())
-                    .arg(status);
-            }
-        } catch (const Exception& ex) {
-            LOG_ERROR(options4_logger, EVAL_RESULT)
-                .arg((*it)->getName())
-                .arg(ex.what());
-        } catch (...) {
-            LOG_ERROR(options4_logger, EVAL_RESULT)
-                .arg((*it)->getName())
-                .arg("get exception?");
-        }
-    }
+    Dhcpv4Exchange::classifyPacket(pkt);
 }
 
 void Dhcpv4Srv::requiredClassify(Dhcpv4Exchange& ex) {
index 72680fff36e85f773dcc610d57a3e41d63653c30..7cca53143097c3e165554bdb016ae987750d0198 100644 (file)
@@ -128,10 +128,52 @@ public:
     void setReservedMessageFields();
 
     /// @brief Assigns classes retrieved from host reservation database.
-    void setReservedClientClasses();
+    ///
+    /// @param context pointer to the context.
+    static void setReservedClientClasses(AllocEngine::ClientContext4Ptr context);
+
+    /// @brief Assigns classes retrieved from host reservation database
+    /// if they haven't been yet set.
+    ///
+    /// This function sets reserved client classes in case they haven't
+    /// been set after fetching host reservations from the database.
+    /// This is the case when the client has non-global host reservation
+    /// and the selected subnet belongs to a shared network.
+    void conditionallySetReservedClientClasses();
+
+    /// @brief Assigns incoming packet to zero or more classes.
+    ///
+    /// @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, the 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
+    static void classifyPacket(const Pkt4Ptr& pkt);
 
 private:
 
+    /// @brief Assign class using vendor-class-identifier option
+    ///
+    /// @note This is the first part of @ref classifyPacket
+    ///
+    /// @param pkt packet to be classified
+    static void classifyByVendor(const Pkt4Ptr& pkt);
+
+    /// @brief Evaluate classes.
+    ///
+    /// @note Second part of the classification.
+    ///
+    /// Evaluate expressions of client classes: if it returns true the class
+    /// is added to the incoming packet.
+    ///
+    /// @param pkt packet to be classified.
+    /// @param depend_on_known if false classes depending on the KNOWN or
+    /// UNKNOWN classes are skipped, if true only these classes are evaluated.
+    static void evaluateClasses(const Pkt4Ptr& pkt, bool depend_on_known);
+
     /// @brief Copies default parameters from client's to server's message
     ///
     /// Some fields are copied from client's message into server's response,
@@ -652,6 +694,8 @@ protected:
     /// server's response.
     void processClientName(Dhcpv4Exchange& ex);
 
+public:
+
     /// @brief this is a prefix added to the content of vendor-class option
     ///
     /// If incoming packet has a vendor class option, its content is
@@ -899,20 +943,6 @@ protected:
     /// @param pkt packet to be classified
     void classifyPacket(const Pkt4Ptr& pkt);
 
-public:
-
-    /// @brief Evaluate classes.
-    ///
-    /// @note Second part of the classification.
-    ///
-    /// Evaluate expressions of client classes: if it returns true the class
-    /// is added to the incoming packet.
-    ///
-    /// @param pkt packet to be classified.
-    /// @param depend_on_known if false classes depending on the KNOWN or
-    /// UNKNOWN classes are skipped, if true only these classes are evaluated.
-    static void evaluateClasses(const Pkt4Ptr& pkt, bool depend_on_known);
-
 protected:
 
     /// @brief Assigns incoming packet to zero or more classes (required pass).
@@ -961,7 +991,6 @@ protected:
 
 private:
 
-    /// @public
     /// @brief Assign class using vendor-class-identifier option
     ///
     /// @note This is the first part of @ref classifyPacket
index 9b1aa6c904738be7d84a5292559541c4396e0640..65cee16bfe6ba72e0ffc676db78578c66bd9eabc 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2018-2020 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
@@ -166,6 +166,146 @@ const char* CONFIGS[] = {
         "    }\n"
         "]\n"
         "}\n"
+    ,
+
+    // Configuration 4 client-class reservation in global, shared network
+    // and client-class guarded pools.
+    "{ \"interfaces-config\": {\n"
+        "      \"interfaces\": [ \"*\" ]\n"
+        "},\n"
+        "\"client-classes\": ["
+        "{"
+        "     \"name\": \"reserved_class\""
+        "},"
+        "{"
+        "     \"name\": \"unreserved_class\","
+        "     \"test\": \"not member('reserved_class')\""
+        "}"
+        "],\n"
+        "\"reservation-mode\": \"global\","
+        "\"valid-lifetime\": 600,\n"
+        "\"reservations\": [ \n"
+        "{\n"
+        "   \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+        "   \"client-classes\": [ \"reserved_class\" ]\n"
+        "}\n"
+        "],\n"
+        "\"shared-networks\": [{"
+        "    \"name\": \"frog\",\n"
+        "    \"subnet4\": [\n"
+        "        {\n"
+        "            \"subnet\": \"10.0.0.0/24\", \n"
+        "            \"id\": 10,"
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"10.0.0.10-10.0.0.11\","
+        "                    \"client-class\": \"reserved_class\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        },\n"
+        "        {\n"
+        "            \"subnet\": \"192.0.3.0/24\", \n"
+        "            \"id\": 11,"
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"192.0.3.10-192.0.3.11\","
+        "                    \"client-class\": \"unreserved_class\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        }\n"
+        "    ]\n"
+        "}]\n"
+    "}",
+
+    // Configuration 5 client-class reservation in global, shared network
+    // and client-class guarded subnets.
+    "{ \"interfaces-config\": {\n"
+        "      \"interfaces\": [ \"*\" ]\n"
+        "},\n"
+        "\"client-classes\": ["
+        "{"
+        "     \"name\": \"reserved_class\""
+        "},"
+        "{"
+        "     \"name\": \"unreserved_class\","
+        "     \"test\": \"not member('reserved_class')\""
+        "}"
+        "],\n"
+        "\"reservation-mode\": \"global\","
+        "\"valid-lifetime\": 600,\n"
+        "\"reservations\": [ \n"
+        "{\n"
+        "   \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+        "   \"client-classes\": [ \"reserved_class\" ]\n"
+        "}\n"
+        "],\n"
+        "\"shared-networks\": [{"
+        "    \"name\": \"frog\",\n"
+        "    \"subnet4\": [\n"
+        "        {\n"
+        "            \"subnet\": \"10.0.0.0/24\", \n"
+        "            \"id\": 10,"
+        "            \"client-class\": \"reserved_class\","
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"10.0.0.10-10.0.0.10\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        },\n"
+        "        {\n"
+        "            \"subnet\": \"192.0.3.0/24\", \n"
+        "            \"id\": 11,"
+        "            \"client-class\": \"unreserved_class\","
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"192.0.3.10-192.0.3.10\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        }\n"
+        "    ]\n"
+        "}]\n"
+    "}",
+
+    // Configuration 6 client-class reservation and client-class guarded pools.
+    "{ \"interfaces-config\": {\n"
+        "      \"interfaces\": [ \"*\" ]\n"
+        "},\n"
+        "\"client-classes\": ["
+        "{"
+        "     \"name\": \"reserved_class\""
+        "},"
+        "{"
+        "     \"name\": \"unreserved_class\","
+        "     \"test\": \"not member('reserved_class')\""
+        "}"
+        "],\n"
+        "\"valid-lifetime\": 600,\n"
+        "\"subnet4\": [\n"
+        "    {\n"
+        "        \"subnet\": \"10.0.0.0/24\", \n"
+        "        \"id\": 10,"
+        "        \"reservations\": [{ \n"
+        "            \"hw-address\": \"aa:bb:cc:dd:ee:fe\",\n"
+        "            \"client-classes\": [ \"reserved_class\" ]\n"
+        "        }],\n"
+        "        \"pools\": ["
+        "            {"
+        "                \"pool\": \"10.0.0.10-10.0.0.11\","
+        "                \"client-class\": \"reserved_class\""
+        "            },"
+        "            {"
+        "                \"pool\": \"10.0.0.20-10.0.0.21\","
+        "                \"client-class\": \"unreserved_class\""
+        "            }"
+        "        ],\n"
+        "        \"interface\": \"eth0\"\n"
+        "    }\n"
+        "]\n"
+    "}"
 };
 
 /// @brief Test fixture class for testing global v4 reservations.
@@ -206,7 +346,8 @@ public:
     /// @param expected_addr expected address to be assigned
     void runDoraTest(const std::string& config, Dhcp4Client& client,
                      const std::string& expected_host,
-                     const std::string& expected_addr) {
+                     const std::string& expected_addr,
+                     const std::string& requested_addr = "") {
 
         // Configure DHCP server.
         ASSERT_NO_FATAL_FAILURE(configure(config, *client.getServer()));
@@ -214,7 +355,11 @@ public:
 
         // Perform 4-way exchange with the server but to not request any
         // specific address in the DHCPDISCOVER message.
-        ASSERT_NO_THROW(client.doDORA());
+        boost::shared_ptr<IOAddress> hint; 
+        if (!requested_addr.empty()) {
+            hint = boost::make_shared<IOAddress>(requested_addr);
+        }
+        ASSERT_NO_THROW(client.doDORA(hint));
 
         // Make sure that the server responded.
         ASSERT_TRUE(client.getContext().response_);
@@ -237,7 +382,54 @@ public:
         EXPECT_EQ(client.config_.lease_.addr_.toText(), expected_addr);
     }
 
-
+    /// @brief Test pool or subnet selection using global class reservation.
+    ///
+    /// Verifies that client class specified in the global reservation
+    /// may be used to influence pool or subnet selection.
+    ///
+    /// @param config_idx Index of the server configuration from the
+    /// @c CONFIGS array.
+    /// @param first_address Address to be allocated from the pool having
+    /// a reservation.
+    /// @param second_address Address to be allocated from the pool not
+    /// having a reservation.
+    void testGlobalClassSubnetPoolSelection(const int config_idx,
+                                            const std::string& first_address = "10.0.0.10",
+                                            const std::string& second_address = "192.0.3.10") {
+        Dhcp4Client client_resrv(Dhcp4Client::SELECTING);
+
+        // Use HW address for which we have host reservation including
+        // client class.
+        client_resrv.setHWAddress("aa:bb:cc:dd:ee:fe");
+        client_resrv.setIfaceName("eth0");
+
+        ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[config_idx], *client_resrv.getServer()));
+
+        // This client should be given an address from the 10.0.0.0/24 pool.
+        // Let's use the 192.0.3.10 as a hint to make sure that the server
+        // refuses allocating it and uses the sole pool available for this
+        // client.
+        ASSERT_NO_THROW(client_resrv.doDORA(boost::make_shared<IOAddress>(second_address)));
+        ASSERT_TRUE(client_resrv.getContext().response_);
+        auto resp = client_resrv.getContext().response_;
+        ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+        EXPECT_EQ(first_address, resp->getYiaddr().toText());
+
+        // This client has no reservation and therefore should be
+        // assigned to the unreserved_class and be given an address
+        // from the other pool.
+        Dhcp4Client client_no_resrv(client_resrv.getServer(), Dhcp4Client::SELECTING);
+        client_no_resrv.setHWAddress("aa:bb:cc:dd:ee:ff");
+        client_no_resrv.setIfaceName("eth0");
+
+        // Let's use the address of 10.0.0.10 as a hint to make sure that the
+        // server refuses it in favor of the 192.0.3.10.
+        ASSERT_NO_THROW(client_no_resrv.doDORA(boost::make_shared<IOAddress>(first_address)));
+        ASSERT_TRUE(client_no_resrv.getContext().response_);
+        resp = client_no_resrv.getContext().response_;
+        ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+        EXPECT_EQ(second_address, resp->getYiaddr().toText());
+    }
 };
 
 // Verifies that a client, which fails to match to a global
@@ -377,4 +569,22 @@ TEST_F(HostTest, allOverGlobal) {
     runDoraTest(CONFIGS[3], client, "subnet-10-host", "192.0.5.10");
 }
 
+// Verifies that client class specified in the global reservation
+// may be used to influence pool selection.
+TEST_F(HostTest, clientClassGlobalPoolSelection) {
+    ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(4));
+}
+
+// Verifies that client class specified in the global reservation
+// may be used to influence subnet selection within shared network.
+TEST_F(HostTest, clientClassGlobalSubnetSelection) {
+    ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(5));
+}
+
+// Verifies that client class specified in the reservation may be
+// used to influence pool selection within a subnet.
+TEST_F(HostTest, clientClassPoolSelection) {
+    ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(6, "10.0.0.10", "10.0.0.20"));
+}
+
 } // end of anonymous namespace
index fd761877ef6795b50cb801e43898b8ead358fcd2..855e33383c4d637b8dbd77c9d3b56a0d7bcceae0 100644 (file)
@@ -323,6 +323,7 @@ Dhcpv6Srv::initContext(const Pkt6Ptr& pkt,
     // Collect host identifiers if host reservations enabled. The identifiers
     // are stored in order of preference. The server will use them in that
     // order to search for host reservations.
+    SharedNetwork6Ptr sn;
     if (ctx.subnet_) {
         const ConstCfgHostOperationsPtr cfg =
             CfgMgr::instance().getCurrentCfg()->getCfgHostOperations6();
@@ -385,6 +386,43 @@ Dhcpv6Srv::initContext(const Pkt6Ptr& pkt,
 
         // Find host reservations using specified identifiers.
         alloc_engine_->findReservation(ctx);
+
+        // Get shared network to see if it is set for a subnet.
+        ctx.subnet_->getSharedNetwork(sn);
+    }
+
+    // Global host reservations are independent of a selected subnet. If the
+    // global reservations contain client classes we should use them in case
+    // they are meant to affect pool selection. Also, if the subnet does not
+    // belong to a shared network we can use the reserved client classes
+    // because there is no way our subnet could change. Such classes may
+    // affect selection of a pool within the selected subnet.
+    auto global_host = ctx.globalHost();
+    auto current_host = ctx.currentHost();
+    if ((global_host && !global_host->getClientClasses6().empty()) ||
+        (!sn && current_host && !current_host->getClientClasses6().empty())) {
+        // We have already evaluated client classes and some of them may
+        // be in conflict with the reserved classes. Suppose there are
+        // two classes defined in the server configuration: first_class
+        // and second_class and the test for the second_class it looks
+        // like this: "not member('first_class')". If the first_class
+        // initially evaluates to false, the second_class evaluates to
+        // true. If the first_class is now set within the hosts reservations
+        // and we don't remove the previously evaluated second_class we'd
+        // end up with both first_class and second_class evaluated to
+        // true. In order to avoid that, we have to remove the classes
+        // evaluated in the first pass and evaluate them again. As
+        // a result, the first_class set via the host reservation will
+        // replace the second_class because the second_class will this
+        // time evaluate to false as desired.
+        const ClientClassDictionaryPtr& dict =
+            CfgMgr::instance().getCurrentCfg()->getClientClassDictionary();
+        const ClientClassDefListPtr& defs_ptr = dict->getClasses();
+        for (auto def : *defs_ptr) {
+            ctx.query_->classes_.erase(def->getName());
+        }
+        setReservedClientClasses(pkt, ctx);
+        evaluateClasses(pkt,  false);
     }
 
     // Set KNOWN builtin class if something was found, UNKNOWN if not.
@@ -2863,7 +2901,7 @@ Dhcpv6Srv::processSolicit(AllocEngine::ClientContext6& ctx) {
     processClientFqdn(solicit, response, ctx);
     assignLeases(solicit, response, ctx);
 
-    setReservedClientClasses(solicit, ctx);
+    conditionallySetReservedClientClasses(solicit, ctx);
     requiredClassify(solicit, ctx);
 
     copyClientOptions(solicit, response);
@@ -2893,7 +2931,7 @@ Dhcpv6Srv::processRequest(AllocEngine::ClientContext6& ctx) {
     processClientFqdn(request, reply, ctx);
     assignLeases(request, reply, ctx);
 
-    setReservedClientClasses(request, ctx);
+    conditionallySetReservedClientClasses(request, ctx);
     requiredClassify(request, ctx);
 
     copyClientOptions(request, reply);
@@ -2919,7 +2957,7 @@ Dhcpv6Srv::processRenew(AllocEngine::ClientContext6& ctx) {
     processClientFqdn(renew, reply, ctx);
     extendLeases(renew, reply, ctx);
 
-    setReservedClientClasses(renew, ctx);
+    conditionallySetReservedClientClasses(renew, ctx);
     requiredClassify(renew, ctx);
 
     copyClientOptions(renew, reply);
@@ -2945,7 +2983,7 @@ Dhcpv6Srv::processRebind(AllocEngine::ClientContext6& ctx) {
     processClientFqdn(rebind, reply, ctx);
     extendLeases(rebind, reply, ctx);
 
-    setReservedClientClasses(rebind, ctx);
+    conditionallySetReservedClientClasses(rebind, ctx);
     requiredClassify(rebind, ctx);
 
     copyClientOptions(rebind, reply);
@@ -2966,7 +3004,7 @@ Pkt6Ptr
 Dhcpv6Srv::processConfirm(AllocEngine::ClientContext6& ctx) {
 
     Pkt6Ptr confirm = ctx.query_;
-    setReservedClientClasses(confirm, ctx);
+    conditionallySetReservedClientClasses(confirm, ctx);
     requiredClassify(confirm, ctx);
 
     // Get IA_NAs from the Confirm. If there are none, the message is
@@ -3056,7 +3094,7 @@ Pkt6Ptr
 Dhcpv6Srv::processRelease(AllocEngine::ClientContext6& ctx) {
 
     Pkt6Ptr release = ctx.query_;
-    setReservedClientClasses(release, ctx);
+    conditionallySetReservedClientClasses(release, ctx);
     requiredClassify(release, ctx);
 
     // Create an empty Reply message.
@@ -3082,7 +3120,7 @@ Pkt6Ptr
 Dhcpv6Srv::processDecline(AllocEngine::ClientContext6& ctx) {
 
     Pkt6Ptr decline = ctx.query_;
-    setReservedClientClasses(decline, ctx);
+    conditionallySetReservedClientClasses(decline, ctx);
     requiredClassify(decline, ctx);
 
     // Create an empty Reply message.
@@ -3372,7 +3410,7 @@ Pkt6Ptr
 Dhcpv6Srv::processInfRequest(AllocEngine::ClientContext6& ctx) {
 
     Pkt6Ptr inf_request = ctx.query_;
-    setReservedClientClasses(inf_request, ctx);
+    conditionallySetReservedClientClasses(inf_request, ctx);
     requiredClassify(inf_request, ctx);
 
     // Create a Reply packet, with the same trans-id as the client's.
@@ -3523,6 +3561,18 @@ Dhcpv6Srv::setReservedClientClasses(const Pkt6Ptr& pkt,
     }
 }
 
+void
+Dhcpv6Srv::conditionallySetReservedClientClasses(const Pkt6Ptr& pkt,
+                                                 const AllocEngine::ClientContext6& ctx) {
+    if (ctx.subnet_) {
+        SharedNetwork6Ptr shared_network;
+        ctx.subnet_->getSharedNetwork(shared_network);
+        if (shared_network && !ctx.globalHost()) {
+            setReservedClientClasses(pkt, ctx);
+        }
+    }
+}
+
 void
 Dhcpv6Srv::requiredClassify(const Pkt6Ptr& pkt, AllocEngine::ClientContext6& ctx) {
     // First collect required classes
index e421832d03ea236ed50d121992e8d3ea4cb22875..e30d72bdfb27e83b728bb88add0147b6e407d292 100644 (file)
@@ -745,7 +745,7 @@ protected:
     ///
     /// @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
+    /// calling @ref classifyByVendor(). Second, the 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).
@@ -772,6 +772,19 @@ protected:
     void setReservedClientClasses(const Pkt6Ptr& pkt,
                                   const AllocEngine::ClientContext6& ctx);
 
+    /// @brief Assigns classes retrieved from host reservation database
+    /// if they haven't been yet set.
+    ///
+    /// This function sets reserved client classes in case they haven't
+    /// been set after fetching host reservations from the database.
+    /// This is the case when the client has non-global host reservation
+    /// and the selected subnet belongs to a shared network.
+    ///
+    /// @param pkt Pointer to the packet to which classes will be assigned.
+    /// @param ctx Reference to the client context.
+    void conditionallySetReservedClientClasses(const Pkt6Ptr& pkt,
+                                               const AllocEngine::ClientContext6& ctx);
+
     /// @brief Assigns incoming packet to zero or more classes (required pass).
     ///
     /// @note This required classification evaluates all classes which
index 596470f4c7673adf25b1c644ba453db8de838c79..49b6b2da03d6498a43fe51492cec4a1d95d2f866 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2018 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2020 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
@@ -419,7 +419,148 @@ const char* CONFIGS[] = {
         "    }] \n"
         " }"
         " ] \n"
-    "} \n"
+    "} \n",
+
+    // Configuration 10: client-class reservation in global, shared network
+    // and client-class guarded pools.
+    "{ \"interfaces-config\": {\n"
+        "      \"interfaces\": [ \"*\" ]\n"
+        "},\n"
+        "\"host-reservation-identifiers\": [ \"duid\", \"hw-address\" ], \n"
+        "\"client-classes\": ["
+        "{"
+        "     \"name\": \"reserved_class\""
+        "},"
+        "{"
+        "     \"name\": \"unreserved_class\","
+        "     \"test\": \"not member('reserved_class')\""
+        "}"
+        "],\n"
+        "\"reservation-mode\": \"global\","
+        "\"valid-lifetime\": 4000,\n"
+        "\"reservations\": [ \n"
+        "{\n"
+        "   \"duid\": \"01:02:03:05\",\n"
+        "   \"client-classes\": [ \"reserved_class\" ]\n"
+        "}\n"
+        "],\n"
+        "\"shared-networks\": [{"
+        "    \"name\": \"frog\",\n"
+        "    \"subnet6\": [\n"
+        "        {\n"
+        "            \"subnet\": \"2001:db8:1::/64\", \n"
+        "            \"id\": 10,"
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"2001:db8:1::10-2001:db8:1::11\","
+        "                    \"client-class\": \"reserved_class\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        },\n"
+        "        {\n"
+        "            \"subnet\": \"2001:db8:2::/64\", \n"
+        "            \"id\": 11,"
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"2001:db8:2::10-2001:db8:2::11\","
+        "                    \"client-class\": \"unreserved_class\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        }\n"
+        "    ]\n"
+        "}]\n"
+    "}",
+
+    // Configuration 11: client-class reservation in global, shared network
+    // and client-class guarded subnets.
+    "{ \"interfaces-config\": {\n"
+        "      \"interfaces\": [ \"*\" ]\n"
+        "},\n"
+        "\"host-reservation-identifiers\": [ \"duid\", \"hw-address\" ], \n"
+        "\"client-classes\": ["
+        "{"
+        "     \"name\": \"reserved_class\""
+        "},"
+        "{"
+        "     \"name\": \"unreserved_class\","
+        "     \"test\": \"not member('reserved_class')\""
+        "}"
+        "],\n"
+        "\"reservation-mode\": \"global\","
+        "\"valid-lifetime\": 4000,\n"
+        "\"reservations\": [ \n"
+        "{\n"
+        "   \"duid\": \"01:02:03:05\",\n"
+        "   \"client-classes\": [ \"reserved_class\" ]\n"
+        "}\n"
+        "],\n"
+    "\"shared-networks\": [{"
+        "    \"name\": \"frog\",\n"
+        "    \"subnet6\": [\n"
+        "        {\n"
+        "            \"subnet\": \"2001:db8:1::/64\", \n"
+        "            \"client-class\": \"reserved_class\","
+        "            \"id\": 10,"
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"2001:db8:1::10-2001:db8:1::11\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        },\n"
+        "        {\n"
+        "            \"subnet\": \"2001:db8:2::/64\", \n"
+        "            \"client-class\": \"unreserved_class\","
+        "            \"id\": 11,"
+        "            \"pools\": ["
+        "                {"
+        "                    \"pool\": \"2001:db8:2::10-2001:db8:2::11\""
+        "                }"
+        "            ],\n"
+        "            \"interface\": \"eth0\"\n"
+        "        }\n"
+        "    ]\n"
+        "}]\n"
+    "}",
+
+    // Configuration 12 client-class reservation and client-class guarded pools.
+    "{ \"interfaces-config\": {\n"
+        "      \"interfaces\": [ \"*\" ]\n"
+        "},\n"
+        "\"client-classes\": ["
+        "{"
+        "     \"name\": \"reserved_class\""
+        "},"
+        "{"
+        "     \"name\": \"unreserved_class\","
+        "     \"test\": \"not member('reserved_class')\""
+        "}"
+        "],\n"
+        "\"valid-lifetime\": 4000,\n"
+        "\"subnet6\": [\n"
+        "    {\n"
+        "        \"subnet\": \"2001:db8:1::/64\", \n"
+        "        \"id\": 10,"
+        "        \"reservations\": [{ \n"
+        "            \"duid\": \"01:02:03:05\",\n"
+        "            \"client-classes\": [ \"reserved_class\" ]\n"
+        "        }],\n"
+        "        \"pools\": ["
+        "            {"
+        "                \"pool\": \"2001:db8:1::10-2001:db8:1::11\","
+        "                \"client-class\": \"reserved_class\""
+        "            },"
+        "            {"
+        "                \"pool\": \"2001:db8:1::20-2001:db8:1::21\","
+        "                \"client-class\": \"unreserved_class\""
+        "            }"
+        "        ],\n"
+        "        \"interface\": \"eth0\"\n"
+        "    }\n"
+        "]\n"
+    "}"
 };
 
 /// @brief Base class representing leases and hints conveyed within IAs.
@@ -813,6 +954,21 @@ public:
     /// @param hint Const reference to an object holding the hint.
     static void requestIA(Dhcp6Client& client, const Hint& hint);
 
+    /// @brief Test pool or subnet selection using global class reservation.
+    ///
+    /// Verifies that client class specified in the global reservation
+    /// may be used to influence pool or subnet selection.
+    ///
+    /// @param config_idx Index of the server configuration from the
+    /// @c CONFIGS array.
+    /// @param first_address Address to be allocated from the pool having
+    /// a reservation.
+    /// @param second_address Address to be allocated from the pool not
+    /// having a reservation.
+    void testGlobalClassSubnetPoolSelection(const int config_idx,
+                                            const std::string& first_address = "2001:db8:1::10",
+                                            const std::string& second_address = "2001:db8:2::10");
+
     /// @brief Configures client to include 6 IAs without hints.
     ///
     /// This method configures the client to include 3 IA_NAs and
@@ -1158,6 +1314,42 @@ HostTest::testOverrideVendorOptions(const uint16_t msg_type) {
     EXPECT_EQ("3000:1::234", addrs[0].toText());
 }
 
+void
+HostTest::testGlobalClassSubnetPoolSelection(const int config_idx,
+                                             const std::string& first_address,
+                                             const std::string& second_address) {
+    Dhcp6Client client_resrv;
+
+    // Use DUID for which we have host reservation including client class.
+    client_resrv.setDUID("01:02:03:05");
+
+    ASSERT_NO_FATAL_FAILURE(configure(CONFIGS[config_idx], *client_resrv.getServer()));
+
+    // This client should be given an address from the 2001:db8:1::/64 subnet.
+    // Let's use the 2001:db8:2::10 as a hint to make sure that the server
+    // refuses allocating it and uses the sole pool available for this
+    // client.
+    client_resrv.requestAddress(1, IOAddress(second_address));
+    ASSERT_NO_THROW(client_resrv.doSARR());
+    ASSERT_EQ(1, client_resrv.getLeaseNum());
+    Lease6 lease_client = client_resrv.getLease(0);
+    EXPECT_EQ(first_address, lease_client.addr_.toText());
+
+    // This client has no reservation and therefore should be
+    // assigned to the unreserved_class and be given an address
+    // from the other pool.
+    Dhcp6Client client_no_resrv(client_resrv.getServer());
+    client_no_resrv.setDUID("01:02:03:04");
+
+    // Let's use the address of 2001:db8:1::10 as a hint to make sure that the
+    // server refuses it in favor of the 2001:db8:2::10.
+    client_no_resrv.requestAddress(1, IOAddress(first_address));
+    ASSERT_NO_THROW(client_no_resrv.doSARR());
+    ASSERT_EQ(1, client_no_resrv.getLeaseNum());
+    lease_client = client_no_resrv.getLease(0);
+    EXPECT_EQ(second_address, lease_client.addr_.toText());
+}
+
 void
 HostTest::requestEmptyIAs(Dhcp6Client& client) {
     // Create IAs with IAIDs between 1 and 6.
@@ -2147,4 +2339,23 @@ TEST_F(HostTest, globalReservationsPD) {
     }
 }
 
+// Verifies that client class specified in the global reservation
+// may be used to influence pool selection.
+TEST_F(HostTest, clientClassGlobalPoolSelection) {
+    ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(10));
+}
+
+// Verifies that client class specified in the global reservation
+// may be used to influence subnet selection within shared network.
+TEST_F(HostTest, clientClassGlobalSubnetSelection) {
+    ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(11));
+}
+
+// Verifies that client class specified in the reservation may be
+// used to influence pool selection within a subnet.
+TEST_F(HostTest, clientClassPoolSelection) {
+    ASSERT_NO_FATAL_FAILURE(testGlobalClassSubnetPoolSelection(12, "2001:db8:1::10",
+                                                               "2001:db8:1::20"));
+}
+
 } // end of anonymous namespace
index e234598e9404358a8e9112a02c48f6130f3e6965..65cd8308ce62e1b6085507544378d24a5001af7f 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 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
@@ -30,6 +30,12 @@ ClientClasses::ClientClasses(const std::string& class_names)
     }
 }
 
+void
+ClientClasses::erase(const ClientClass& class_name) {
+    list_.remove(class_name);
+    static_cast<void>(set_.erase(class_name));
+}
+
 std::string
 ClientClasses::toText(const std::string& separator) const {
     std::stringstream s;
index 14614313fce46a774f6b3413744ee853c7f2ca2f..727e0cc4028ebe52bc0d780f8985746f546e9d4e 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2014-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2014-2020 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
@@ -64,6 +64,11 @@ namespace dhcp {
             set_.insert(class_name);
         }
 
+        /// @brief Erase element by name.
+        ///
+        /// @param class_name The name of the class to erase.
+        void erase(const ClientClass& class_name);
+
         /// @brief Check if classes is empty.
         bool empty() const {
             return (list_.empty());
index 14e9d58443c314ac008d4e196c64e1d3ed2eda7c..938379cc88a3c295806d1b232fadc000a5fc0c69 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2020 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
@@ -131,3 +131,21 @@ TEST(ClassifyTest, ClientClassesToText) {
     // Check non-standard separator.
     EXPECT_EQ("alpha.gamma.beta", classes.toText("."));
 }
+
+// Check that selected class can be erased.
+TEST(ClassifyTest, Erase) {
+    ClientClasses classes;
+
+    classes.insert("alpha");
+    classes.insert("beta");
+    EXPECT_TRUE(classes.contains("alpha"));
+    EXPECT_TRUE(classes.contains("beta"));
+
+    classes.erase("beta");
+    EXPECT_TRUE(classes.contains("alpha"));
+    EXPECT_FALSE(classes.contains("beta"));
+
+    classes.erase("alpha");
+    EXPECT_FALSE(classes.contains("alpha"));
+    EXPECT_FALSE(classes.contains("beta"));
+}
index 9032974972654e756a5cd9cbd22451722b5ae5e3..98669c405a918be47410d2353d155e2429544b27 100644 (file)
@@ -3040,6 +3040,18 @@ AllocEngine::ClientContext4::currentHost() const {
     return (ConstHostPtr());
 }
 
+ConstHostPtr
+AllocEngine::ClientContext4::globalHost() const {
+    if (subnet_ && subnet_->getHostReservationMode() == Network::HR_GLOBAL) {
+        auto host = hosts_.find(SUBNET_ID_GLOBAL);
+        if (host != hosts_.cend()) {
+            return (host->second);
+        }
+    }
+
+    return (ConstHostPtr());
+}
+
 Lease4Ptr
 AllocEngine::allocateLease4(ClientContext4& ctx) {
     // The NULL pointer indicates that the old lease didn't exist. It may
index fee5e23e274b6b5511b9ccb87c439ff24f155877..868408f1f4870c9f4283a1055a56177fa481c9ec 100644 (file)
@@ -1334,6 +1334,16 @@ public:
         /// @return Pointer to the host object.
         ConstHostPtr currentHost() const;
 
+        /// @brief Returns global host reservation if there is one
+        ///
+        /// If the current subnet's reservation mode is global and
+        /// there is a global host (i.e. reservation belonging to
+        /// the global subnet), return it.  Otherwise return an
+        /// empty pointer.
+        ///
+        /// @return Pointer to the host object.
+        ConstHostPtr globalHost() const;
+
         /// @brief Default constructor.
         ClientContext4();