From: Marcin Siodelski Date: Thu, 19 Mar 2020 12:29:45 +0000 (+0100) Subject: [#1155] Backported #1139 from master X-Git-Tag: Kea-1.6.3~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=01d583f6deb98ed368ac200277a9d04cd8788de6;p=thirdparty%2Fkea.git [#1155] Backported #1139 from master 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. --- diff --git a/ChangeLog b/ChangeLog index e29021bd9b..1bdae674d1 100644 --- 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 diff --git a/doc/sphinx/arm/dhcp4-srv.rst b/doc/sphinx/arm/dhcp4-srv.rst index 65e2ccadc1..b5cb846c24 100644 --- a/doc/sphinx/arm/dhcp4-srv.rst +++ b/doc/sphinx/arm/dhcp4-srv.rst @@ -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 diff --git a/doc/sphinx/arm/dhcp6-srv.rst b/doc/sphinx/arm/dhcp6-srv.rst index 5a4046212f..edf3497ce0 100644 --- a/doc/sphinx/arm/dhcp6-srv.rst +++ b/doc/sphinx/arm/dhcp6-srv.rst @@ -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 diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index fab7cd4405..a203bb3774 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/src/bin/dhcp4/dhcp4_srv.cc @@ -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 vendor_class = + boost::dynamic_pointer_cast(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 vendor_class = - boost::dynamic_pointer_cast(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) { diff --git a/src/bin/dhcp4/dhcp4_srv.h b/src/bin/dhcp4/dhcp4_srv.h index 72680fff36..7cca531430 100644 --- a/src/bin/dhcp4/dhcp4_srv.h +++ b/src/bin/dhcp4/dhcp4_srv.h @@ -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 diff --git a/src/bin/dhcp4/tests/host_unittest.cc b/src/bin/dhcp4/tests/host_unittest.cc index 9b1aa6c904..65cee16bfe 100644 --- a/src/bin/dhcp4/tests/host_unittest.cc +++ b/src/bin/dhcp4/tests/host_unittest.cc @@ -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 hint; + if (!requested_addr.empty()) { + hint = boost::make_shared(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(second_address))); + ASSERT_TRUE(client_resrv.getContext().response_); + auto resp = client_resrv.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast(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(first_address))); + ASSERT_TRUE(client_no_resrv.getContext().response_); + resp = client_no_resrv.getContext().response_; + ASSERT_EQ(DHCPACK, static_cast(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 diff --git a/src/bin/dhcp6/dhcp6_srv.cc b/src/bin/dhcp6/dhcp6_srv.cc index fd761877ef..855e33383c 100644 --- a/src/bin/dhcp6/dhcp6_srv.cc +++ b/src/bin/dhcp6/dhcp6_srv.cc @@ -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 diff --git a/src/bin/dhcp6/dhcp6_srv.h b/src/bin/dhcp6/dhcp6_srv.h index e421832d03..e30d72bdfb 100644 --- a/src/bin/dhcp6/dhcp6_srv.h +++ b/src/bin/dhcp6/dhcp6_srv.h @@ -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 diff --git a/src/bin/dhcp6/tests/host_unittest.cc b/src/bin/dhcp6/tests/host_unittest.cc index 596470f4c7..49b6b2da03 100644 --- a/src/bin/dhcp6/tests/host_unittest.cc +++ b/src/bin/dhcp6/tests/host_unittest.cc @@ -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 diff --git a/src/lib/dhcp/classify.cc b/src/lib/dhcp/classify.cc index e234598e94..65cd8308ce 100644 --- a/src/lib/dhcp/classify.cc +++ b/src/lib/dhcp/classify.cc @@ -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(set_.erase(class_name)); +} + std::string ClientClasses::toText(const std::string& separator) const { std::stringstream s; diff --git a/src/lib/dhcp/classify.h b/src/lib/dhcp/classify.h index 14614313fc..727e0cc402 100644 --- a/src/lib/dhcp/classify.h +++ b/src/lib/dhcp/classify.h @@ -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()); diff --git a/src/lib/dhcp/tests/classify_unittest.cc b/src/lib/dhcp/tests/classify_unittest.cc index 14e9d58443..938379cc88 100644 --- a/src/lib/dhcp/tests/classify_unittest.cc +++ b/src/lib/dhcp/tests/classify_unittest.cc @@ -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")); +} diff --git a/src/lib/dhcpsrv/alloc_engine.cc b/src/lib/dhcpsrv/alloc_engine.cc index 9032974972..98669c405a 100644 --- a/src/lib/dhcpsrv/alloc_engine.cc +++ b/src/lib/dhcpsrv/alloc_engine.cc @@ -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 diff --git a/src/lib/dhcpsrv/alloc_engine.h b/src/lib/dhcpsrv/alloc_engine.h index fee5e23e27..868408f1f4 100644 --- a/src/lib/dhcpsrv/alloc_engine.h +++ b/src/lib/dhcpsrv/alloc_engine.h @@ -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();