Browse Source

[master] Merge branch 'trac2995' (4 initial hooks for DHCPv6)

Tomek Mrugalski 11 years ago
parent
commit
0f8f7995fa

+ 1 - 0
doc/devel/mainpage.dox

@@ -52,6 +52,7 @@
  *   - @subpage dhcpv6Session
  *   - @subpage dhcpv6Session
  *   - @subpage dhcpv6ConfigParser
  *   - @subpage dhcpv6ConfigParser
  *   - @subpage dhcpv6ConfigInherit
  *   - @subpage dhcpv6ConfigInherit
+ *   - @subpage dhcpv6Hooks
  * - @subpage libdhcp
  * - @subpage libdhcp
  *   - @subpage libdhcpIntro
  *   - @subpage libdhcpIntro
  *   - @subpage libdhcpRelay
  *   - @subpage libdhcpRelay

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

@@ -63,6 +63,7 @@ b10_dhcp4_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
 b10_dhcp4_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+b10_dhcp4_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 
 
 b10_dhcp4dir = $(pkgdatadir)
 b10_dhcp4dir = $(pkgdatadir)
 b10_dhcp4_DATA = dhcp4.spec
 b10_dhcp4_DATA = dhcp4.spec

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

@@ -71,6 +71,7 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
 dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 endif
 endif
 
 
 noinst_PROGRAMS = $(TESTS)
 noinst_PROGRAMS = $(TESTS)

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

@@ -26,6 +26,7 @@
 #include <dhcp/pkt_filter_inet.h>
 #include <dhcp/pkt_filter_inet.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <dhcp4/dhcp4_srv.h>
 #include <dhcp4/dhcp4_log.h>
 #include <dhcp4/dhcp4_log.h>
+#include <hooks/server_hooks.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/lease_mgr_factory.h>
@@ -155,6 +156,9 @@ public:
         unlink(SRVID_FILE);
         unlink(SRVID_FILE);
     }
     }
 
 
+    virtual ~Dhcpv4SrvTest() {
+    }
+
     /// @brief Add 'Parameter Request List' option to the packet.
     /// @brief Add 'Parameter Request List' option to the packet.
     ///
     ///
     /// This function PRL option comprising the following option codes:
     /// This function PRL option comprising the following option codes:

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

@@ -65,6 +65,7 @@ b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
 b10_dhcp6_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+b10_dhcp6_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 
 
 b10_dhcp6dir = $(pkgdatadir)
 b10_dhcp6dir = $(pkgdatadir)
 b10_dhcp6_DATA = dhcp6.spec
 b10_dhcp6_DATA = dhcp6.spec

+ 137 - 0
src/bin/dhcp6/dhcp6_hooks.dox

@@ -0,0 +1,137 @@
+// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ @page dhcpv6Hooks The Hooks API for the DHCPv6 Server
+
+ @section dhcpv6HooksIntroduction Introduction
+ BIND10 features an API (the "Hooks" API) that allows user-written code to
+ be integrated into BIND 10 and called at specific points in its processing.
+ An overview of the API and a tutorial for writing such code can be found in
+ the @ref hooksDevelopersGuide.  Information for BIND 10 maintainers can be
+ found in the @ref hooksComponentDeveloperGuide.
+
+ This manual is more specialised and is aimed at developers of hook
+ code for the DHCPv6 server. It describes each hook point, what the callouts
+ attached to the hook are able to do, and the arguments passed to the
+ callouts.  Each entry in this manual has the following information:
+
+ - Name of the hook point.
+ - Arguments for the callout.  As well as the argument name and data type, the
+   information includes the direction, which can be one of:
+   - @b in - the server passes values to the callout but ignored any data
+     returned.
+   - @b out - the callout is expected to set this value.
+   - <b>in/out</b> - the server passes a value to the callout and uses whatever
+     value the callout sends back.  Note that the callout may choose not to
+     do any modification, in which case the server will use whatever value
+     it sent to the callout.
+ - Description of the hook. This explains where in the processing the hook
+   is located, the possible actions a callout attached to this hook could take,
+   and a description of the data passed to the callouts.
+ - Skip flag action: the action taken by the server if a callout chooses to set
+    the "skip" flag.
+
+@section dhcpv6HooksHookPoints Hooks in the DHCPv6 Server
+
+The following list is ordered by appearance of specific hook points during
+packet processing. Hook points that are not specific to packet processing
+(e.g. lease expiration) will be added to the end of this list.
+
+ @subsection dhcpv6HooksPkt6Receive pkt6_receive
+
+ - @b Arguments:
+   - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when an incoming DHCPv6
+   packet is received and its content is parsed. The sole argument -
+   query6 - contains a pointer to an isc::dhcp::Pkt6 object that contains
+   all information regarding incoming packet, including its source and
+   destination addresses, interface over which it was received, a list
+   of all options present within and relay information.  All fields of
+   the Pkt6 object can be modified at this time, except data_. (data_
+   contains the incoming packet as raw buffer. By the time this hook is
+   reached, that information has already parsed and is available though
+   other fields in the Pkt6 object.  For this reason, it doesn't make
+   sense to modify it.)
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server will
+   drop the packet and start processing the next one.  The reason for the drop
+   will be logged if logging is set to the appropriate debug level.
+
+@subsection dhcpv6HooksSubnet6Select subnet6_select
+
+ - @b Arguments:
+   - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+   - name: @b subnet6, type: isc::dhcp::Subnet6Ptr, direction: <b>in/out</b>
+   - name: @b subnet6collection, type: const isc::dhcp::Subnet6Collection *, direction: <b>in</b>
+
+ - @b Description: this callout is executed when a subnet is being
+   selected for the incoming packet. All parameters, addresses and
+   prefixes will be assigned from that subnet. A callout can select a
+   different subnet if it wishes so, the list of all subnets currently
+   configured being provided as 'subnet6collection'. The list itself must
+   not be modified.
+
+ - <b>Skip flag action</b>: If any callout installed on 'subnet6_select'
+   sets the skip flag, the server will not select any subnet. Packet processing
+   will continue, but will be severely limited (i.e. only global options
+   will be assigned).
+
+@subsection dhcpv6HooksLeaseSelect lease6_select
+
+ - @b Arguments:
+   - name: @b subnet6, type: isc::dhcp::Subnet6Ptr, direction: <b>in</b>
+   - name: @b fake_allocation, type: bool, direction: <b>in</b>
+   - name: @b lease6, type: isc::dhcp::Lease6Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed after the server engine
+   has selected a lease for client's request but before the lease
+   has been inserted into the database. Any modifications made to the
+   isc::dhcp::Lease6 object will be stored in the lease's record in the
+   database. The callout should make sure that any modifications are
+   sanity checked as the server will use that data as is with no further
+   checking.\n\n The server processes lease requests for SOLICIT and
+   REQUEST in a very similar way. The only major difference is that
+   for SOLICIT the lease is just selected; it is not inserted into
+   the database.  It is possible to distinguish between SOLICIT and
+   REQUEST by checking value of the fake_allocation flag: a value of true
+   means that the lease won't be inserted into the database (SOLICIT),
+   a value of false means that it will (REQUEST).
+
+ - <b>Skip flag action</b>: the "skip" flag is ignored by the server on this
+   hook.
+
+@subsection dhcpv6HooksPkt6Send pkt6_send
+
+ - @b Arguments:
+   - name: @b response6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when server's response
+   is about to be send back to the client. The sole argument - response6 -
+   contains a pointer to an isc::dhcp::Pkt6 object that contains the
+   packet, with set source and destination addresses, interface over which
+   it will be send, list of all options and relay information.  All fields
+   of the Pkt6 object can be modified at this time, except bufferOut_.
+   (This is scratch space used for constructing the packet after all
+   pkt6_send callouts are complete, so any changes to that field will
+   be overwritten.)
+
+ - <b>Skip flag action</b>: if any callout sets the skip flag, the server
+   will drop this response packet. However, the original request packet
+   from a client was processed, so server's state was most likely changed
+   (e.g. lease was allocated). Setting this flag merely stops the change
+   being communicated to the client.
+
+*/

+ 3 - 0
src/bin/dhcp6/dhcp6_log.h

@@ -38,6 +38,9 @@ const int DBG_DHCP6_COMMAND = DBGLVL_COMMAND;
 // Trace basic operations within the code.
 // Trace basic operations within the code.
 const int DBG_DHCP6_BASIC = DBGLVL_TRACE_BASIC;
 const int DBG_DHCP6_BASIC = DBGLVL_TRACE_BASIC;
 
 
+// Trace hook related operations
+const int DBG_DHCP6_HOOKS = DBGLVL_TRACE_BASIC;
+
 // Trace detailed operations, including errors raised when processing invalid
 // Trace detailed operations, including errors raised when processing invalid
 // packets.  (These are not logged at severities of WARN or higher for fear
 // packets.  (These are not logged at severities of WARN or higher for fear
 // that a set of deliberately invalid packets set to the server could overwhelm
 // that a set of deliberately invalid packets set to the server could overwhelm

+ 19 - 0
src/bin/dhcp6/dhcp6_messages.mes

@@ -65,6 +65,25 @@ This informational message is printed every time the IPv6 DHCP server
 is started.  It indicates what database backend type is being to store
 is started.  It indicates what database backend type is being to store
 lease and other information.
 lease and other information.
 
 
+% DHCP6_HOOK_PACKET_RCVD_SKIP received DHCPv6 packet was dropped, because a callout set skip flag.
+This debug message is printed when a callout installed on pkt6_received
+hook point sets skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to drop the packet.
+
+% DHCP6_HOOK_PACKET_SEND_SKIP Prepared DHCPv6 response was not sent, because a callout set skip flag.
+This debug message is printed when a callout installed on pkt6_send
+hook point sets skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to drop the packet. This
+effectively means that the client will not get any response, even though
+the server processed client's request and acted on it (e.g. possibly
+allocated a lease).
+
+% DHCP6_HOOK_SUBNET6_SELECT_SKIP No subnet was selected, because a callout set skip flag.
+This debug message is printed when a callout installed on subnet6_select
+hook point sets a skip flag. It means that the server was told that no subnet
+should be selected. This severely limits further processing - server will be only
+able to offer global options. No addresses or prefixes could be assigned.
+
 % DHCP6_LEASE_ADVERT lease %1 advertised (client duid=%2, iaid=%3)
 % DHCP6_LEASE_ADVERT lease %1 advertised (client duid=%2, iaid=%3)
 This debug message indicates that the server successfully advertised
 This debug message indicates that the server successfully advertised
 a lease. It is up to the client to choose one server out of the
 a lease. It is up to the client to choose one server out of the

+ 161 - 9
src/bin/dhcp6/dhcp6_srv.cc

@@ -37,6 +37,8 @@
 #include <util/io_utilities.h>
 #include <util/io_utilities.h>
 #include <util/range_utilities.h>
 #include <util/range_utilities.h>
 #include <util/encode/hex.h>
 #include <util/encode/hex.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
 
 
 #include <boost/foreach.hpp>
 #include <boost/foreach.hpp>
 #include <boost/tokenizer.hpp>
 #include <boost/tokenizer.hpp>
@@ -50,9 +52,34 @@
 using namespace isc;
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
 using namespace isc::dhcp;
+using namespace isc::hooks;
 using namespace isc::util;
 using namespace isc::util;
 using namespace std;
 using namespace std;
 
 
+namespace {
+
+/// Structure that holds registered hook indexes
+struct Dhcp6Hooks {
+    int hook_index_pkt6_receive_;   ///< index for "pkt6_receive" hook point
+    int hook_index_subnet6_select_; ///< index for "subnet6_select" hook point
+    int hook_index_pkt6_send_;      ///< index for "pkt6_send" hook point
+
+    /// Constructor that registers hook points for DHCPv6 engine
+    Dhcp6Hooks() {
+        hook_index_pkt6_receive_   = HooksManager::registerHook("pkt6_receive");
+        hook_index_subnet6_select_ = HooksManager::registerHook("subnet6_select");
+        hook_index_pkt6_send_      = HooksManager::registerHook("pkt6_send");
+    }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+Dhcp6Hooks Hooks;
+
+}; // anonymous namespace
+
 namespace isc {
 namespace isc {
 namespace dhcp {
 namespace dhcp {
 
 
@@ -67,7 +94,9 @@ namespace dhcp {
 static const char* SERVER_DUID_FILE = "b10-dhcp6-serverid";
 static const char* SERVER_DUID_FILE = "b10-dhcp6-serverid";
 
 
 Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
 Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
-    : alloc_engine_(), serverid_(), shutdown_(true) {
+:alloc_engine_(), serverid_(), shutdown_(true), hook_index_pkt6_receive_(-1),
+    hook_index_subnet6_select_(-1), hook_index_pkt6_send_(-1)
+{
 
 
     LOG_DEBUG(dhcp6_logger, DBG_DHCP6_START, DHCP6_OPEN_SOCKET).arg(port);
     LOG_DEBUG(dhcp6_logger, DBG_DHCP6_START, DHCP6_OPEN_SOCKET).arg(port);
 
 
@@ -106,6 +135,13 @@ Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
         // Instantiate allocation engine
         // Instantiate allocation engine
         alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
         alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
 
 
+        // Register hook points
+        hook_index_pkt6_receive_   = Hooks.hook_index_pkt6_receive_;
+        hook_index_subnet6_select_ = Hooks.hook_index_subnet6_select_;
+        hook_index_pkt6_send_      = Hooks.hook_index_pkt6_send_;
+
+        /// @todo call loadLibraries() when handling configuration changes
+
     } catch (const std::exception &e) {
     } catch (const std::exception &e) {
         LOG_ERROR(dhcp6_logger, DHCP6_SRV_CONSTRUCT_ERROR).arg(e.what());
         LOG_ERROR(dhcp6_logger, DHCP6_SRV_CONSTRUCT_ERROR).arg(e.what());
         return;
         return;
@@ -126,9 +162,17 @@ void Dhcpv6Srv::shutdown() {
     shutdown_ = true;
     shutdown_ = true;
 }
 }
 
 
+Pkt6Ptr Dhcpv6Srv::receivePacket(int timeout) {
+    return (IfaceMgr::instance().receive6(timeout));
+}
+
+void Dhcpv6Srv::sendPacket(const Pkt6Ptr& packet) {
+    IfaceMgr::instance().send(packet);
+}
+
 bool Dhcpv6Srv::run() {
 bool Dhcpv6Srv::run() {
     while (!shutdown_) {
     while (!shutdown_) {
-        /// @todo: calculate actual timeout to the next event (e.g. lease
+        /// @todo Calculate actual timeout to the next event (e.g. lease
         /// expiration) once we have lease database. The idea here is that
         /// expiration) once we have lease database. The idea here is that
         /// it is possible to do everything in a single process/thread.
         /// it is possible to do everything in a single process/thread.
         /// For now, we are just calling select for 1000 seconds. There
         /// For now, we are just calling select for 1000 seconds. There
@@ -142,7 +186,7 @@ bool Dhcpv6Srv::run() {
         Pkt6Ptr rsp;
         Pkt6Ptr rsp;
 
 
         try {
         try {
-            query = IfaceMgr::instance().receive6(timeout);
+            query = receivePacket(timeout);
         } catch (const std::exception& e) {
         } catch (const std::exception& e) {
             LOG_ERROR(dhcp6_logger, DHCP6_PACKET_RECEIVE_FAIL).arg(e.what());
             LOG_ERROR(dhcp6_logger, DHCP6_PACKET_RECEIVE_FAIL).arg(e.what());
         }
         }
@@ -160,6 +204,30 @@ bool Dhcpv6Srv::run() {
                       .arg(query->getBuffer().getLength())
                       .arg(query->getBuffer().getLength())
                       .arg(query->toText());
                       .arg(query->toText());
 
 
+            // Let's execute all callouts registered for packet_received
+            if (HooksManager::getHooksManager().calloutsPresent(hook_index_pkt6_receive_)) {
+                CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+                // Delete previously set arguments
+                callout_handle->deleteAllArguments();
+
+                // Pass incoming packet as argument
+                callout_handle->setArgument("query6", query);
+
+                // Call callouts
+                HooksManager::callCallouts(hook_index_pkt6_receive_, *callout_handle);
+
+                // Callouts decided to skip the next processing step. The next
+                // processing step would to process the packet, so skip at this
+                // stage means drop.
+                if (callout_handle->getSkip()) {
+                    LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_PACKET_RCVD_SKIP);
+                    continue;
+                }
+
+                callout_handle->getArgument("query6", query);
+            }
+
             try {
             try {
                 switch (query->getType()) {
                 switch (query->getType()) {
                 case DHCPV6_SOLICIT:
                 case DHCPV6_SOLICIT:
@@ -204,7 +272,7 @@ bool Dhcpv6Srv::run() {
             } catch (const RFCViolation& e) {
             } catch (const RFCViolation& e) {
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_REQUIRED_OPTIONS_CHECK_FAIL)
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_REQUIRED_OPTIONS_CHECK_FAIL)
                     .arg(query->getName())
                     .arg(query->getName())
-                    .arg(query->getRemoteAddr())
+                    .arg(query->getRemoteAddr().toText())
                     .arg(e.what());
                     .arg(e.what());
 
 
             } catch (const isc::Exception& e) {
             } catch (const isc::Exception& e) {
@@ -218,7 +286,7 @@ bool Dhcpv6Srv::run() {
                 // packets.)
                 // packets.)
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_PACKET_PROCESS_FAIL)
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_PACKET_PROCESS_FAIL)
                     .arg(query->getName())
                     .arg(query->getName())
-                    .arg(query->getRemoteAddr())
+                    .arg(query->getRemoteAddr().toText())
                     .arg(e.what());
                     .arg(e.what());
             }
             }
 
 
@@ -230,13 +298,35 @@ bool Dhcpv6Srv::run() {
                 rsp->setIndex(query->getIndex());
                 rsp->setIndex(query->getIndex());
                 rsp->setIface(query->getIface());
                 rsp->setIface(query->getIface());
 
 
+                // Execute all callouts registered for packet6_send
+                if (HooksManager::getHooksManager().calloutsPresent(hook_index_pkt6_send_)) {
+                    CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+                    // Delete all previous arguments
+                    callout_handle->deleteAllArguments();
+
+                    // Set our response
+                    callout_handle->setArgument("response6", rsp);
+
+                    // Call all installed callouts
+                    HooksManager::callCallouts(hook_index_pkt6_send_, *callout_handle);
+
+                    // Callouts decided to skip the next processing step. The next
+                    // processing step would to send the packet, so skip at this
+                    // stage means "drop response".
+                    if (callout_handle->getSkip()) {
+                        LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_PACKET_SEND_SKIP);
+                        continue;
+                    }
+                }
+
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA,
                 LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA,
                           DHCP6_RESPONSE_DATA)
                           DHCP6_RESPONSE_DATA)
                     .arg(static_cast<int>(rsp->getType())).arg(rsp->toText());
                     .arg(static_cast<int>(rsp->getType())).arg(rsp->toText());
 
 
                 if (rsp->pack()) {
                 if (rsp->pack()) {
                     try {
                     try {
-                        IfaceMgr::instance().send(rsp);
+                        sendPacket(rsp);
                     } catch (const std::exception& e) {
                     } catch (const std::exception& e) {
                         LOG_ERROR(dhcp6_logger, DHCP6_PACKET_SEND_FAIL).arg(e.what());
                         LOG_ERROR(dhcp6_logger, DHCP6_PACKET_SEND_FAIL).arg(e.what());
                     }
                     }
@@ -560,6 +650,37 @@ Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
         }
         }
     }
     }
 
 
+    // Let's execute all callouts registered for packet_received
+    if (HooksManager::getHooksManager().calloutsPresent(hook_index_subnet6_select_)) {
+        CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
+        // We're reusing callout_handle from previous calls
+        callout_handle->deleteAllArguments();
+
+        // Set new arguments
+        callout_handle->setArgument("query6", question);
+        callout_handle->setArgument("subnet6", subnet);
+
+        // We pass pointer to const collection for performance reasons.
+        // Otherwise we would get a non-trivial performance penalty each
+        // time subnet6_select is called.
+        callout_handle->setArgument("subnet6collection", CfgMgr::instance().getSubnets6());
+
+        // Call user (and server-side) callouts
+        HooksManager::callCallouts(hook_index_subnet6_select_, *callout_handle);
+
+        // Callouts decided to skip this step. This means that no subnet will be
+        // selected. Packet processing will continue, but it will be severly limited
+        // (i.e. only global options will be assigned)
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_SUBNET6_SELECT_SKIP);
+            return (Subnet6Ptr());
+        }
+
+        // Use whatever subnet was specified by the callout
+        callout_handle->getArgument("subnet6", subnet);
+    }
+
     return (subnet);
     return (subnet);
 }
 }
 
 
@@ -619,7 +740,8 @@ Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
         switch (opt->second->getType()) {
         switch (opt->second->getType()) {
         case D6O_IA_NA: {
         case D6O_IA_NA: {
             OptionPtr answer_opt = assignIA_NA(subnet, duid, question,
             OptionPtr answer_opt = assignIA_NA(subnet, duid, question,
-                                   boost::dynamic_pointer_cast<Option6IA>(opt->second));
+                                               boost::dynamic_pointer_cast<Option6IA>(opt->second),
+                                               question);
             if (answer_opt) {
             if (answer_opt) {
                 answer->addOption(answer_opt);
                 answer->addOption(answer_opt);
             }
             }
@@ -633,7 +755,7 @@ Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
 
 
 OptionPtr
 OptionPtr
 Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
 Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
-                       Pkt6Ptr question, boost::shared_ptr<Option6IA> ia) {
+                       Pkt6Ptr question, boost::shared_ptr<Option6IA> ia, const Pkt6Ptr& query) {
     // If there is no subnet selected for handling this IA_NA, the only thing to do left is
     // If there is no subnet selected for handling this IA_NA, the only thing to do left is
     // to say that we are sorry, but the user won't get an address. As a convenience, we
     // to say that we are sorry, but the user won't get an address. As a convenience, we
     // use a different status text to indicate that (compare to the same status code,
     // use a different status text to indicate that (compare to the same status code,
@@ -677,12 +799,15 @@ Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
         fake_allocation = true;
         fake_allocation = true;
     }
     }
 
 
+    CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
     // Use allocation engine to pick a lease for this client. Allocation engine
     // Use allocation engine to pick a lease for this client. Allocation engine
     // will try to honour the hint, but it is just a hint - some other address
     // will try to honour the hint, but it is just a hint - some other address
     // may be used instead. If fake_allocation is set to false, the lease will
     // may be used instead. If fake_allocation is set to false, the lease will
     // be inserted into the LeaseMgr as well.
     // be inserted into the LeaseMgr as well.
     Lease6Ptr lease = alloc_engine_->allocateAddress6(subnet, duid, ia->getIAID(),
     Lease6Ptr lease = alloc_engine_->allocateAddress6(subnet, duid, ia->getIAID(),
-                                                      hint, fake_allocation);
+                                                      hint, fake_allocation,
+                                                      callout_handle);
 
 
     // Create IA_NA that we will put in the response.
     // Create IA_NA that we will put in the response.
     // Do not use OptionDefinition to create option's instance so
     // Do not use OptionDefinition to create option's instance so
@@ -1103,5 +1228,32 @@ Dhcpv6Srv::processInfRequest(const Pkt6Ptr& infRequest) {
     return reply;
     return reply;
 }
 }
 
 
+isc::hooks::CalloutHandlePtr Dhcpv6Srv::getCalloutHandle(const Pkt6Ptr& pkt) {
+    // This method returns a CalloutHandle for a given packet. It is guaranteed
+    // to return the same callout_handle (so user library contexts are
+    // preserved). This method works well if the server processes one packet
+    // at a time. Once the server architecture is extended to cover parallel
+    // packets processing (e.g. delayed-ack, some form of buffering etc.), this
+    // method has to be extended (e.g. store callouts in a map and use pkt as
+    // a key). Additional code would be required to release the callout handle
+    // once the server finished processing.
+
+    CalloutHandlePtr callout_handle;
+    static Pkt6Ptr old_pointer;
+
+    if (!callout_handle ||
+        old_pointer != pkt) {
+        // This is the first packet or a different packet than previously
+        // passed to getCalloutHandle()
+
+        // Remember the pointer to this packet
+        old_pointer = pkt;
+
+        callout_handle = HooksManager::getHooksManager().createCalloutHandle();
+    }
+
+    return (callout_handle);
+}
+
 };
 };
 };
 };

+ 28 - 1
src/bin/dhcp6/dhcp6_srv.h

@@ -23,6 +23,7 @@
 #include <dhcp/pkt6.h>
 #include <dhcp/pkt6.h>
 #include <dhcpsrv/alloc_engine.h>
 #include <dhcpsrv/alloc_engine.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/subnet.h>
+#include <hooks/callout_handle.h>
 
 
 #include <boost/noncopyable.hpp>
 #include <boost/noncopyable.hpp>
 
 
@@ -189,7 +190,8 @@ protected:
     OptionPtr assignIA_NA(const isc::dhcp::Subnet6Ptr& subnet,
     OptionPtr assignIA_NA(const isc::dhcp::Subnet6Ptr& subnet,
                           const isc::dhcp::DuidPtr& duid,
                           const isc::dhcp::DuidPtr& duid,
                           isc::dhcp::Pkt6Ptr question,
                           isc::dhcp::Pkt6Ptr question,
-                          boost::shared_ptr<Option6IA> ia);
+                          boost::shared_ptr<Option6IA> ia,
+                          const Pkt6Ptr& query);
 
 
     /// @brief Renews specific IA_NA option
     /// @brief Renews specific IA_NA option
     ///
     ///
@@ -321,6 +323,19 @@ protected:
     /// @return string representation
     /// @return string representation
     static std::string duidToString(const OptionPtr& opt);
     static std::string duidToString(const OptionPtr& opt);
 
 
+
+    /// @brief dummy wrapper around IfaceMgr::receive6
+    ///
+    /// This method is useful for testing purposes, where its replacement
+    /// simulates reception of a packet. For that purpose it is protected.
+    virtual Pkt6Ptr receivePacket(int timeout);
+
+    /// @brief dummy wrapper around IfaceMgr::send()
+    ///
+    /// This method is useful for testing purposes, where its replacement
+    /// simulates transmission of a packet. For that purpose it is protected.
+    virtual void sendPacket(const Pkt6Ptr& pkt);
+
 private:
 private:
     /// @brief Allocation Engine.
     /// @brief Allocation Engine.
     /// Pointer to the allocation engine that we are currently using
     /// Pointer to the allocation engine that we are currently using
@@ -334,6 +349,18 @@ private:
     /// Indicates if shutdown is in progress. Setting it to true will
     /// Indicates if shutdown is in progress. Setting it to true will
     /// initiate server shutdown procedure.
     /// initiate server shutdown procedure.
     volatile bool shutdown_;
     volatile bool shutdown_;
+
+    /// @brief returns callout handle for specified packet
+    ///
+    /// @param pkt packet for which the handle should be returned
+    ///
+    /// @return a callout handle to be used in hooks related to said packet
+    isc::hooks::CalloutHandlePtr getCalloutHandle(const Pkt6Ptr& pkt);
+
+    /// Indexes for registered hook points
+    int hook_index_pkt6_receive_;
+    int hook_index_subnet6_select_;
+    int hook_index_pkt6_send_;
 };
 };
 
 
 }; // namespace isc::dhcp
 }; // namespace isc::dhcp

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

@@ -68,6 +68,7 @@ dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
 dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 endif
 endif
 
 
 noinst_PROGRAMS = $(TESTS)
 noinst_PROGRAMS = $(TESTS)

+ 781 - 1
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -24,8 +24,10 @@
 #include <dhcp/option6_ia.h>
 #include <dhcp/option6_ia.h>
 #include <dhcp/option6_iaaddr.h>
 #include <dhcp/option6_iaaddr.h>
 #include <dhcp/option_int_array.h>
 #include <dhcp/option_int_array.h>
+#include <dhcp/iface_mgr.h>
 #include <dhcp6/config_parser.h>
 #include <dhcp6/config_parser.h>
 #include <dhcp6/dhcp6_srv.h>
 #include <dhcp6/dhcp6_srv.h>
+#include <dhcp/dhcp6.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/lease_mgr_factory.h>
@@ -33,12 +35,16 @@
 #include <util/buffer.h>
 #include <util/buffer.h>
 #include <util/range_utilities.h>
 #include <util/range_utilities.h>
 
 
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
+
 #include <boost/scoped_ptr.hpp>
 #include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 #include <unistd.h>
 #include <unistd.h>
 #include <fstream>
 #include <fstream>
 #include <iostream>
 #include <iostream>
 #include <sstream>
 #include <sstream>
+#include <list>
 
 
 using namespace isc;
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::asiolink;
@@ -46,6 +52,7 @@ using namespace isc::config;
 using namespace isc::data;
 using namespace isc::data;
 using namespace isc::dhcp;
 using namespace isc::dhcp;
 using namespace isc::util;
 using namespace isc::util;
+using namespace isc::hooks;
 using namespace std;
 using namespace std;
 
 
 // namespace has to be named, because friends are defined in Dhcpv6Srv class
 // namespace has to be named, because friends are defined in Dhcpv6Srv class
@@ -61,6 +68,46 @@ public:
         LeaseMgrFactory::create(memfile);
         LeaseMgrFactory::create(memfile);
     }
     }
 
 
+    /// @brief fakes packet reception
+    /// @param timeout ignored
+    ///
+    /// The method receives all packets queued in receive
+    /// queue, one after another. Once the queue is empty,
+    /// it initiates the shutdown procedure.
+    ///
+    /// See fake_received_ field for description
+    virtual Pkt6Ptr receivePacket(int /*timeout*/) {
+
+        // If there is anything prepared as fake incoming
+        // traffic, use it
+        if (!fake_received_.empty()) {
+            Pkt6Ptr pkt = fake_received_.front();
+            fake_received_.pop_front();
+            return (pkt);
+        }
+
+        // If not, just trigger shutdown and
+        // return immediately
+        shutdown();
+        return (Pkt6Ptr());
+    }
+
+    /// @brief fake packet sending
+    ///
+    /// Pretend to send a packet, but instead just store
+    /// it in fake_send_ list where test can later inspect
+    /// server's response.
+    virtual void sendPacket(const Pkt6Ptr& pkt) {
+        fake_sent_.push_back(pkt);
+    }
+
+    /// @brief adds a packet to fake receive queue
+    ///
+    /// See fake_received_ field for description
+    void fakeReceive(const Pkt6Ptr& pkt) {
+        fake_received_.push_back(pkt);
+    }
+
     virtual ~NakedDhcpv6Srv() {
     virtual ~NakedDhcpv6Srv() {
         // Close the lease database
         // Close the lease database
         LeaseMgrFactory::destroy();
         LeaseMgrFactory::destroy();
@@ -75,6 +122,16 @@ public:
     using Dhcpv6Srv::sanityCheck;
     using Dhcpv6Srv::sanityCheck;
     using Dhcpv6Srv::loadServerID;
     using Dhcpv6Srv::loadServerID;
     using Dhcpv6Srv::writeServerID;
     using Dhcpv6Srv::writeServerID;
+
+    /// @brief packets we pretend to receive
+    ///
+    /// Instead of setting up sockets on interfaces that change between OSes, it
+    /// is much easier to fake packet reception. This is a list of packets that
+    /// we pretend to have received. You can schedule new packets to be received
+    /// using fakeReceive() and NakedDhcpv6Srv::receivePacket() methods.
+    list<Pkt6Ptr> fake_received_;
+
+    list<Pkt6Ptr> fake_sent_;
 };
 };
 
 
 static const char* DUID_FILE = "server-id-test.txt";
 static const char* DUID_FILE = "server-id-test.txt";
@@ -87,6 +144,16 @@ public:
     NakedDhcpv6SrvTest() : rcode_(-1) {
     NakedDhcpv6SrvTest() : rcode_(-1) {
         // it's ok if that fails. There should not be such a file anyway
         // it's ok if that fails. There should not be such a file anyway
         unlink(DUID_FILE);
         unlink(DUID_FILE);
+
+        const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+
+        // There must be some interface detected
+        if (ifaces.empty()) {
+            // We can't use ASSERT in constructor
+            ADD_FAILURE() << "No interfaces detected.";
+        }
+
+        valid_iface_ = ifaces.begin()->getName();
     }
     }
 
 
     // Generate IA_NA option with specified parameters
     // Generate IA_NA option with specified parameters
@@ -228,6 +295,13 @@ public:
     virtual ~NakedDhcpv6SrvTest() {
     virtual ~NakedDhcpv6SrvTest() {
         // Let's clean up if there is such a file.
         // Let's clean up if there is such a file.
         unlink(DUID_FILE);
         unlink(DUID_FILE);
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                                                 "pkt6_receive");
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                                                 "pkt6_send");
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+                                                 "subnet6_select");
+
     };
     };
 
 
     // A DUID used in most tests (typically as client-id)
     // A DUID used in most tests (typically as client-id)
@@ -235,6 +309,9 @@ public:
 
 
     int rcode_;
     int rcode_;
     ConstElementPtr comment_;
     ConstElementPtr comment_;
+
+    // Name of a valid network interface
+    string valid_iface_;
 };
 };
 
 
 // Provides suport for tests against a preconfigured subnet6
 // Provides suport for tests against a preconfigured subnet6
@@ -480,7 +557,7 @@ TEST_F(Dhcpv6SrvTest, DUID) {
 
 
     boost::scoped_ptr<Dhcpv6Srv> srv;
     boost::scoped_ptr<Dhcpv6Srv> srv;
     ASSERT_NO_THROW( {
     ASSERT_NO_THROW( {
-        srv.reset(new Dhcpv6Srv(0));
+        srv.reset(new NakedDhcpv6Srv(0));
     });
     });
 
 
     OptionPtr srvid = srv->getServerID();
     OptionPtr srvid = srv->getServerID();
@@ -1756,6 +1833,709 @@ TEST_F(Dhcpv6SrvTest, ServerID) {
     EXPECT_EQ(duid1_text, text);
     EXPECT_EQ(duid1_text, text);
 }
 }
 
 
+// Checks if hooks are implemented properly.
+TEST_F(Dhcpv6SrvTest, Hooks) {
+    NakedDhcpv6Srv srv(0);
+
+    // check if appropriate hooks are registered
+    int hook_index_pkt6_received = -1;
+    int hook_index_select_subnet = -1;
+    int hook_index_pkt6_send     = -1;
+
+    // check if appropriate indexes are set
+    EXPECT_NO_THROW(hook_index_pkt6_received = ServerHooks::getServerHooks()
+                    .getIndex("pkt6_receive"));
+    EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks()
+                    .getIndex("subnet6_select"));
+    EXPECT_NO_THROW(hook_index_pkt6_send     = ServerHooks::getServerHooks()
+                    .getIndex("pkt6_send"));
+
+    EXPECT_TRUE(hook_index_pkt6_received > 0);
+    EXPECT_TRUE(hook_index_select_subnet > 0);
+    EXPECT_TRUE(hook_index_pkt6_send > 0);
+}
+
+// This function returns buffer for empty packet (just DHCPv6 header)
+Pkt6* captureEmpty() {
+    Pkt6* pkt;
+    uint8_t data[4];
+    data[0] = 1; // type 1 = SOLICIT
+    data[1] = 0xca; // trans-id = 0xcafe01
+    data[2] = 0xfe;
+    data[3] = 0x01;
+
+    pkt = new Pkt6(data, sizeof(data));
+    pkt->setRemotePort(546);
+    pkt->setRemoteAddr(IOAddress("fe80::1"));
+    pkt->setLocalPort(0);
+    pkt->setLocalAddr(IOAddress("ff02::1:2"));
+    pkt->setIndex(2);
+    pkt->setIface("eth0");
+
+    return (pkt);
+}
+
+// This function returns buffer for very simple Solicit
+Pkt6* captureSimpleSolicit() {
+    Pkt6* pkt;
+    uint8_t data[] = {
+        1,  // type 1 = SOLICIT
+        0xca, 0xfe, 0x01, // trans-id = 0xcafe01
+        0, 1, // option type 1 (client-id)
+        0, 10, // option lenth 10
+        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // DUID
+        0, 3, // option type 3 (IA_NA)
+        0, 12, // option length 12
+        0, 0, 0, 1, // iaid = 1
+        0, 0, 0, 0, // T1 = 0
+        0, 0, 0, 0  // T2 = 0
+    };
+
+    pkt = new Pkt6(data, sizeof(data));
+    pkt->setRemotePort(546);
+    pkt->setRemoteAddr(IOAddress("fe80::1"));
+    pkt->setLocalPort(0);
+    pkt->setLocalAddr(IOAddress("ff02::1:2"));
+    pkt->setIndex(2);
+    pkt->setIface("eth0");
+
+    return (pkt);
+}
+
+/// @brief a class dedicated to Hooks testing in DHCPv6 server
+///
+/// This class has a number of static members, because each non-static
+/// method has implicit 'this' parameter, so it does not match callout
+/// signature and couldn't be registered. Furthermore, static methods
+/// can't modify non-static members (for obvious reasons), so many
+/// fields are declared static. It is still better to keep them as
+/// one class rather than unrelated collection of global objects.
+class HooksDhcpv6SrvTest : public Dhcpv6SrvTest {
+
+public:
+
+    /// @brief creates Dhcpv6Srv and prepares buffers for callouts
+    HooksDhcpv6SrvTest() {
+
+        // Allocate new DHCPv6 Server
+        srv_ = new NakedDhcpv6Srv(0);
+
+        // clear static buffers
+        resetCalloutBuffers();
+    }
+
+    /// @brief destructor (deletes Dhcpv6Srv)
+    ~HooksDhcpv6SrvTest() {
+        delete srv_;
+    }
+
+    /// @brief creates an option with specified option code
+    ///
+    /// This method is static, because it is used from callouts
+    /// that do not have a pointer to HooksDhcpv6SSrvTest object
+    ///
+    /// @param option_code code of option to be created
+    ///
+    /// @return pointer to create option object
+    static OptionPtr createOption(uint16_t option_code) {
+
+        char payload[] = {
+            0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14
+        };
+
+        OptionBuffer tmp(payload, payload + sizeof(payload));
+        return OptionPtr(new Option(Option::V6, option_code, tmp));
+    }
+
+    /// test callback that stores received callout name and pkt6 value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("pkt6_receive");
+
+        callout_handle.getArgument("query6", callback_pkt6_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// test callback that changes client-id value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_change_clientid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("query6", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(D6O_CLIENTID);
+
+        // add a new option
+        pkt->addOption(createOption(D6O_CLIENTID));
+
+        // carry on as usual
+        return pkt6_receive_callout(callout_handle);
+    }
+
+    /// test callback that deletes client-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_delete_clientid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("query6", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(D6O_CLIENTID);
+
+        // carry on as usual
+        return pkt6_receive_callout(callout_handle);
+    }
+
+    /// test callback that sets skip flag
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_receive_skip(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("query6", pkt);
+
+        callout_handle.setSkip(true);
+
+        // carry on as usual
+        return pkt6_receive_callout(callout_handle);
+    }
+
+    /// Test callback that stores received callout name and pkt6 value
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("pkt6_send");
+
+        callout_handle.getArgument("response6", callback_pkt6_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    // Test callback that changes server-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_change_serverid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("response6", pkt);
+
+        // get rid of the old server-id
+        pkt->delOption(D6O_SERVERID);
+
+        // add a new option
+        pkt->addOption(createOption(D6O_SERVERID));
+
+        // carry on as usual
+        return pkt6_send_callout(callout_handle);
+    }
+
+    /// test callback that deletes server-id
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_delete_serverid(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("response6", pkt);
+
+        // get rid of the old client-id
+        pkt->delOption(D6O_SERVERID);
+
+        // carry on as usual
+        return pkt6_send_callout(callout_handle);
+    }
+
+    /// Test callback that sets skip flag
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    pkt6_send_skip(CalloutHandle& callout_handle) {
+
+        Pkt6Ptr pkt;
+        callout_handle.getArgument("response6", pkt);
+
+        callout_handle.setSkip(true);
+
+        // carry on as usual
+        return pkt6_send_callout(callout_handle);
+    }
+
+    /// Test callback that stores received callout name and subnet6 values
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    subnet6_select_callout(CalloutHandle& callout_handle) {
+        callback_name_ = string("subnet6_select");
+
+        callout_handle.getArgument("query6", callback_pkt6_);
+        callout_handle.getArgument("subnet6", callback_subnet6_);
+        callout_handle.getArgument("subnet6collection", callback_subnet6collection_);
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// Test callback that picks the other subnet if possible.
+    /// @param callout_handle handle passed by the hooks framework
+    /// @return always 0
+    static int
+    subnet6_select_different_subnet_callout(CalloutHandle& callout_handle) {
+
+        // Call the basic calllout to record all passed values
+        subnet6_select_callout(callout_handle);
+
+        const Subnet6Collection* subnets;
+        Subnet6Ptr subnet;
+        callout_handle.getArgument("subnet6", subnet);
+        callout_handle.getArgument("subnet6collection", subnets);
+
+        // Let's change to a different subnet
+        if (subnets->size() > 1) {
+            subnet = (*subnets)[1]; // Let's pick the other subnet
+            callout_handle.setArgument("subnet6", subnet);
+        }
+
+        return (0);
+    }
+
+    /// resets buffers used to store data received by callouts
+    void resetCalloutBuffers() {
+        callback_name_ = string("");
+        callback_pkt6_.reset();
+        callback_subnet6_.reset();
+        callback_subnet6collection_ = NULL;
+        callback_argument_names_.clear();
+    }
+
+    /// pointer to Dhcpv6Srv that is used in tests
+    NakedDhcpv6Srv* srv_;
+
+    // The following fields are used in testing pkt6_receive_callout
+
+    /// String name of the received callout
+    static string callback_name_;
+
+    /// Pkt6 structure returned in the callout
+    static Pkt6Ptr callback_pkt6_;
+
+    /// Pointer to a subnet received by callout
+    static Subnet6Ptr callback_subnet6_;
+
+    /// A list of all available subnets (received by callout)
+    static const Subnet6Collection* callback_subnet6collection_;
+
+    /// A list of all received arguments
+    static vector<string> callback_argument_names_;
+};
+
+// The following fields are used in testing pkt6_receive_callout.
+// See fields description in the class for details
+string HooksDhcpv6SrvTest::callback_name_;
+Pkt6Ptr HooksDhcpv6SrvTest::callback_pkt6_;
+Subnet6Ptr HooksDhcpv6SrvTest::callback_subnet6_;
+const Subnet6Collection* HooksDhcpv6SrvTest::callback_subnet6collection_;
+vector<string> HooksDhcpv6SrvTest::callback_argument_names_;
+
+
+// Checks if callouts installed on pkt6_received are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "pkt6_receive".
+TEST_F(HooksDhcpv6SrvTest, simple_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_callout));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the callback called is indeed the one we installed
+    EXPECT_EQ("pkt6_receive", callback_name_);
+
+    // check that pkt6 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+    // Check that all expected parameters are there
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back(string("query6"));
+
+    EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt6_received is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_change_clientid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server did send a reposonce
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Make sure that we received a response
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Get client-id...
+    OptionPtr clientid = adv->getOption(D6O_CLIENTID);
+
+    // ... and check if it is the modified value
+    OptionPtr expected = createOption(D6O_CLIENTID);
+    EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt6_received is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv6SrvTest, deleteClientId_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_delete_clientid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // Check that the server dropped the packet and did not send a response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt6_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_pkt6_receive) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_receive", pkt6_receive_skip));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server dropped the packet and did not produce any response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+
+// Checks if callouts installed on pkt6_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv6SrvTest, simple_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_callout));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // Check that the callback called is indeed the one we installed
+    EXPECT_EQ("pkt6_send", callback_name_);
+
+    // Check that there is one packet sent
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+
+    // Check that pkt6 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt6_.get() == adv.get());
+
+    // Check that all expected parameters are there
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back(string("response6"));
+    EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt6_send is able to change
+// the values and the packet sent contains those changes
+TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_change_serverid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server did send a reposonce
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Make sure that we received a response
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Get client-id...
+    OptionPtr clientid = adv->getOption(D6O_SERVERID);
+
+    // ... and check if it is the modified value
+    OptionPtr expected = createOption(D6O_SERVERID);
+    EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt6_send is able to delete
+// existing options and that server applies those changes. In particular,
+// we are trying to send a packet without server-id. The packet should
+// be sent
+TEST_F(HooksDhcpv6SrvTest, deleteServerId_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_delete_serverid));
+
+    // Let's create a simple SOLICIT
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // Check that the server indeed sent a malformed ADVERTISE
+    ASSERT_EQ(1, srv_->fake_sent_.size());
+
+    // Get that ADVERTISE
+    Pkt6Ptr adv = srv_->fake_sent_.front();
+    ASSERT_TRUE(adv);
+
+    // Make sure that it does not have server-id
+    EXPECT_FALSE(adv->getOption(D6O_SERVERID));
+}
+
+// Checks if callouts installed on pkt6_skip is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_pkt6_send) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "pkt6_send", pkt6_send_skip));
+
+    // Let's create a simple REQUEST
+    Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+    // Simulate that we have received that traffic
+    srv_->fakeReceive(sol);
+
+    // Server will now process to run its normal loop, but instead of calling
+    // IfaceMgr::receive6(), it will read all packets from the list set by
+    // fakeReceive()
+    // In particular, it should call registered pkt6_receive callback.
+    srv_->run();
+
+    // check that the server dropped the packet and did not produce any response
+    ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// This test checks if subnet6_select callout is triggered and reports
+// valid parameters
+TEST_F(HooksDhcpv6SrvTest, subnet6_select) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "subnet6_select", subnet6_select_callout));
+
+    // Configure 2 subnets, both directly reachable over local interface
+    // (let's not complicate the matter with relays)
+    string config = "{ \"interface\": [ \"all\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::/64\" ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"" + valid_iface_ + "\" "
+        " }, {"
+        "    \"pool\": [ \"2001:db8:2::/64\" ],"
+        "    \"subnet\": \"2001:db8:2::/48\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json));
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    // Prepare solicit packet. Server should select first subnet for it
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface(valid_iface_);
+    sol->addOption(generateIA(234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Pass it to the server and get an advertise
+    Pkt6Ptr adv = srv_->processSolicit(sol);
+
+    // check if we get response at all
+    ASSERT_TRUE(adv);
+
+    // Check that the callback called is indeed the one we installed
+    EXPECT_EQ("subnet6_select", callback_name_);
+
+    // Check that pkt6 argument passing was successful and returned proper value
+    EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+    const Subnet6Collection* exp_subnets = CfgMgr::instance().getSubnets6();
+
+    // The server is supposed to pick the first subnet, because of matching
+    // interface. Check that the value is reported properly.
+    ASSERT_TRUE(callback_subnet6_);
+    EXPECT_EQ(callback_subnet6_.get(), exp_subnets->front().get());
+
+    // Server is supposed to report two subnets
+    ASSERT_EQ(exp_subnets->size(), callback_subnet6collection_->size());
+
+    // Compare that the available subnets are reported as expected
+    EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet6collection_)[0].get());
+    EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet6collection_)[1].get());
+}
+
+// This test checks if callout installed on subnet6_select hook point can pick
+// a different subnet.
+TEST_F(HooksDhcpv6SrvTest, subnet_select_change) {
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "subnet6_select", subnet6_select_different_subnet_callout));
+
+    // Configure 2 subnets, both directly reachable over local interface
+    // (let's not complicate the matter with relays)
+    string config = "{ \"interface\": [ \"all\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::/64\" ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"interface\": \"" + valid_iface_ + "\" "
+        " }, {"
+        "    \"pool\": [ \"2001:db8:2::/64\" ],"
+        "    \"subnet\": \"2001:db8:2::/48\" "
+        " } ],"
+        "\"valid-lifetime\": 4000 }";
+
+    ElementPtr json = Element::fromJSON(config);
+    ConstElementPtr status;
+
+    // Configure the server and make sure the config is accepted
+    EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json));
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    ASSERT_EQ(0, rcode_);
+
+    // Prepare solicit packet. Server should select first subnet for it
+    Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    sol->setRemoteAddr(IOAddress("fe80::abcd"));
+    sol->setIface(valid_iface_);
+    sol->addOption(generateIA(234, 1500, 3000));
+    OptionPtr clientid = generateClientId();
+    sol->addOption(clientid);
+
+    // Pass it to the server and get an advertise
+    Pkt6Ptr adv = srv_->processSolicit(sol);
+
+    // check if we get response at all
+    ASSERT_TRUE(adv);
+
+    // The response should have an address from second pool, so let's check it
+    OptionPtr tmp = adv->getOption(D6O_IA_NA);
+    ASSERT_TRUE(tmp);
+    boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+    ASSERT_TRUE(ia);
+    tmp = ia->getOption(D6O_IAADDR);
+    ASSERT_TRUE(tmp);
+    boost::shared_ptr<Option6IAAddr> addr_opt =
+        boost::dynamic_pointer_cast<Option6IAAddr>(tmp);
+    ASSERT_TRUE(addr_opt);
+
+    // Get all subnets and use second subnet for verification
+    const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+    ASSERT_EQ(2, subnets->size());
+
+    // Advertised address must belong to the second pool (in subnet's range,
+    // in dynamic pool)
+    EXPECT_TRUE((*subnets)[1]->inRange(addr_opt->getAddress()));
+    EXPECT_TRUE((*subnets)[1]->inPool(addr_opt->getAddress()));
+}
+
+
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// to call processX() methods.
 /// to call processX() methods.
 
 

+ 104 - 5
src/lib/dhcpsrv/alloc_engine.cc

@@ -16,11 +16,35 @@
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 
 
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
+
 #include <cstring>
 #include <cstring>
 #include <vector>
 #include <vector>
 #include <string.h>
 #include <string.h>
 
 
 using namespace isc::asiolink;
 using namespace isc::asiolink;
+using namespace isc::hooks;
+
+namespace {
+
+/// Structure that holds registered hook indexes
+struct Dhcp6Hooks {
+    int hook_index_lease6_select_; ///< index for "lease6_receive" hook point
+
+    /// Constructor that registers hook points for AllocationEngine
+    Dhcp6Hooks() {
+        hook_index_lease6_select_ = HooksManager::registerHook("lease6_select");
+    }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+Dhcp6Hooks Hooks;
+
+}; // anonymous namespace
 
 
 namespace isc {
 namespace isc {
 namespace dhcp {
 namespace dhcp {
@@ -161,6 +185,9 @@ AllocEngine::AllocEngine(AllocType engine_type, unsigned int attempts)
     default:
     default:
         isc_throw(BadValue, "Invalid/unsupported allocation algorithm");
         isc_throw(BadValue, "Invalid/unsupported allocation algorithm");
     }
     }
+
+    // Register hook points
+    hook_index_lease6_select_ = Hooks.hook_index_lease6_select_;
 }
 }
 
 
 Lease6Ptr
 Lease6Ptr
@@ -168,7 +195,8 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
                               const DuidPtr& duid,
                               const DuidPtr& duid,
                               uint32_t iaid,
                               uint32_t iaid,
                               const IOAddress& hint,
                               const IOAddress& hint,
-                              bool fake_allocation /* = false */ ) {
+                              bool fake_allocation,
+                              const isc::hooks::CalloutHandlePtr& callout_handle) {
 
 
     try {
     try {
         // That check is not necessary. We create allocator in AllocEngine
         // That check is not necessary. We create allocator in AllocEngine
@@ -201,7 +229,8 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
                 /// implemented
                 /// implemented
 
 
                 // the hint is valid and not currently used, let's create a lease for it
                 // the hint is valid and not currently used, let's create a lease for it
-                Lease6Ptr lease = createLease6(subnet, duid, iaid, hint, fake_allocation);
+                Lease6Ptr lease = createLease6(subnet, duid, iaid, hint, callout_handle,
+                                               fake_allocation);
 
 
                 // It can happen that the lease allocation failed (we could have lost
                 // It can happen that the lease allocation failed (we could have lost
                 // the race condition. That means that the hint is lo longer usable and
                 // the race condition. That means that the hint is lo longer usable and
@@ -212,7 +241,7 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
             } else {
             } else {
                 if (existing->expired()) {
                 if (existing->expired()) {
                     return (reuseExpiredLease(existing, subnet, duid, iaid,
                     return (reuseExpiredLease(existing, subnet, duid, iaid,
-                                              fake_allocation));
+                                              callout_handle, fake_allocation));
                 }
                 }
 
 
             }
             }
@@ -246,7 +275,7 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
                 // there's no existing lease for selected candidate, so it is
                 // there's no existing lease for selected candidate, so it is
                 // free. Let's allocate it.
                 // free. Let's allocate it.
                 Lease6Ptr lease = createLease6(subnet, duid, iaid, candidate,
                 Lease6Ptr lease = createLease6(subnet, duid, iaid, candidate,
-                                              fake_allocation);
+                                               callout_handle, fake_allocation);
                 if (lease) {
                 if (lease) {
                     return (lease);
                     return (lease);
                 }
                 }
@@ -257,7 +286,7 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
             } else {
             } else {
                 if (existing->expired()) {
                 if (existing->expired()) {
                     return (reuseExpiredLease(existing, subnet, duid, iaid,
                     return (reuseExpiredLease(existing, subnet, duid, iaid,
-                                              fake_allocation));
+                                              callout_handle, fake_allocation));
                 }
                 }
             }
             }
 
 
@@ -438,6 +467,7 @@ Lease6Ptr AllocEngine::reuseExpiredLease(Lease6Ptr& expired,
                                          const Subnet6Ptr& subnet,
                                          const Subnet6Ptr& subnet,
                                          const DuidPtr& duid,
                                          const DuidPtr& duid,
                                          uint32_t iaid,
                                          uint32_t iaid,
+                                         const isc::hooks::CalloutHandlePtr& callout_handle,
                                          bool fake_allocation /*= false */ ) {
                                          bool fake_allocation /*= false */ ) {
 
 
     if (!expired->expired()) {
     if (!expired->expired()) {
@@ -461,6 +491,39 @@ Lease6Ptr AllocEngine::reuseExpiredLease(Lease6Ptr& expired,
     /// @todo: log here that the lease was reused (there's ticket #2524 for
     /// @todo: log here that the lease was reused (there's ticket #2524 for
     /// logging in libdhcpsrv)
     /// logging in libdhcpsrv)
 
 
+    // Let's execute all callouts registered for lease6_select
+    if (callout_handle &&
+        HooksManager::getHooksManager().calloutsPresent(hook_index_lease6_select_)) {
+
+        // Delete all previous arguments
+        callout_handle->deleteAllArguments();
+
+        // Pass necessary arguments
+        // Subnet from which we do the allocation
+        callout_handle->setArgument("subnet6", subnet);
+
+        // Is this solicit (fake = true) or request (fake = false)
+        callout_handle->setArgument("fake_allocation", fake_allocation);
+
+        // The lease that will be assigned to a client
+        callout_handle->setArgument("lease6", expired);
+
+        // Call the callouts
+        HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+        // Callouts decided to skip the action. This means that the lease is not
+        // assigned, so the client will get NoAddrAvail as a result. The lease
+        // won't be inserted into the
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE6_IA_ADD_SKIP);
+            return (Lease6Ptr());
+        }
+
+        // Let's use whatever callout returned. Hopefully it is the same lease
+        // we handled to it.
+        callout_handle->getArgument("lease6", expired);
+    }
+
     if (!fake_allocation) {
     if (!fake_allocation) {
         // for REQUEST we do update the lease
         // for REQUEST we do update the lease
         LeaseMgrFactory::instance().updateLease6(expired);
         LeaseMgrFactory::instance().updateLease6(expired);
@@ -517,12 +580,48 @@ Lease6Ptr AllocEngine::createLease6(const Subnet6Ptr& subnet,
                                     const DuidPtr& duid,
                                     const DuidPtr& duid,
                                     uint32_t iaid,
                                     uint32_t iaid,
                                     const IOAddress& addr,
                                     const IOAddress& addr,
+                                    const isc::hooks::CalloutHandlePtr& callout_handle,
                                     bool fake_allocation /*= false */ ) {
                                     bool fake_allocation /*= false */ ) {
 
 
     Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid, iaid,
     Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid, iaid,
                                subnet->getPreferred(), subnet->getValid(),
                                subnet->getPreferred(), subnet->getValid(),
                                subnet->getT1(), subnet->getT2(), subnet->getID()));
                                subnet->getT1(), subnet->getT2(), subnet->getID()));
 
 
+    // Let's execute all callouts registered for lease6_ia_added
+    if (callout_handle &&
+        HooksManager::getHooksManager().calloutsPresent(hook_index_lease6_select_)) {
+
+        // Delete all previous arguments
+        callout_handle->deleteAllArguments();
+
+        // Clear skip flag if it was set in previous callouts
+        callout_handle->setSkip(false);
+
+        // Pass necessary arguments
+
+        // Subnet from which we do the allocation
+        callout_handle->setArgument("subnet6", subnet);
+
+        // Is this solicit (fake = true) or request (fake = false)
+        callout_handle->setArgument("fake_allocation", fake_allocation);
+        callout_handle->setArgument("lease6", lease);
+
+        // This is the first callout, so no need to clear any arguments
+        HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+        // Callouts decided to skip the action. This means that the lease is not
+        // assigned, so the client will get NoAddrAvail as a result. The lease
+        // won't be inserted into the
+        if (callout_handle->getSkip()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE6_IA_ADD_SKIP);
+            return (Lease6Ptr());
+        }
+
+        // Let's use whatever callout returned. Hopefully it is the same lease
+        // we handled to it.
+        callout_handle->getArgument("lease6", lease);
+    }
+
     if (!fake_allocation) {
     if (!fake_allocation) {
         // That is a real (REQUEST) allocation
         // That is a real (REQUEST) allocation
         bool status = LeaseMgrFactory::instance().addLease(lease);
         bool status = LeaseMgrFactory::instance().addLease(lease);

+ 15 - 1
src/lib/dhcpsrv/alloc_engine.h

@@ -20,6 +20,7 @@
 #include <dhcp/hwaddr.h>
 #include <dhcp/hwaddr.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/subnet.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr.h>
+#include <hooks/callout_handle.h>
 
 
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
 #include <boost/noncopyable.hpp>
 #include <boost/noncopyable.hpp>
@@ -235,13 +236,17 @@ protected:
     /// @param hint a hint that the client provided
     /// @param hint a hint that the client provided
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for SOLICIT that is not really allocated (true)
     ///        an address for SOLICIT that is not really allocated (true)
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
+    ///
     /// @return Allocated IPv6 lease (or NULL if allocation failed)
     /// @return Allocated IPv6 lease (or NULL if allocation failed)
     Lease6Ptr
     Lease6Ptr
     allocateAddress6(const Subnet6Ptr& subnet,
     allocateAddress6(const Subnet6Ptr& subnet,
                      const DuidPtr& duid,
                      const DuidPtr& duid,
                      uint32_t iaid,
                      uint32_t iaid,
                      const isc::asiolink::IOAddress& hint,
                      const isc::asiolink::IOAddress& hint,
-                     bool fake_allocation);
+                     bool fake_allocation,
+                     const isc::hooks::CalloutHandlePtr& callout_handle);
 
 
     /// @brief Destructor. Used during DHCPv6 service shutdown.
     /// @brief Destructor. Used during DHCPv6 service shutdown.
     virtual ~AllocEngine();
     virtual ~AllocEngine();
@@ -276,12 +281,15 @@ private:
     /// @param duid client's DUID
     /// @param duid client's DUID
     /// @param iaid IAID from the IA_NA container the client sent to us
     /// @param iaid IAID from the IA_NA container the client sent to us
     /// @param addr an address that was selected and is confirmed to be available
     /// @param addr an address that was selected and is confirmed to be available
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for SOLICIT that is not really allocated (true)
     ///        an address for SOLICIT that is not really allocated (true)
     /// @return allocated lease (or NULL in the unlikely case of the lease just
     /// @return allocated lease (or NULL in the unlikely case of the lease just
     ///        becomed unavailable)
     ///        becomed unavailable)
     Lease6Ptr createLease6(const Subnet6Ptr& subnet, const DuidPtr& duid,
     Lease6Ptr createLease6(const Subnet6Ptr& subnet, const DuidPtr& duid,
                            uint32_t iaid, const isc::asiolink::IOAddress& addr,
                            uint32_t iaid, const isc::asiolink::IOAddress& addr,
+                           const isc::hooks::CalloutHandlePtr& callout_handle,
                            bool fake_allocation = false);
                            bool fake_allocation = false);
 
 
     /// @brief Reuses expired IPv4 lease
     /// @brief Reuses expired IPv4 lease
@@ -313,12 +321,15 @@ private:
     /// @param subnet subnet the lease is allocated from
     /// @param subnet subnet the lease is allocated from
     /// @param duid client's DUID
     /// @param duid client's DUID
     /// @param iaid IAID from the IA_NA container the client sent to us
     /// @param iaid IAID from the IA_NA container the client sent to us
+    /// @param callout_handle a callout handle (used in hooks). A lease callouts
+    ///        will be executed if this parameter is passed.
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
     ///        an address for SOLICIT that is not really allocated (true)
     ///        an address for SOLICIT that is not really allocated (true)
     /// @return refreshed lease
     /// @return refreshed lease
     /// @throw BadValue if trying to recycle lease that is still valid
     /// @throw BadValue if trying to recycle lease that is still valid
     Lease6Ptr reuseExpiredLease(Lease6Ptr& expired, const Subnet6Ptr& subnet,
     Lease6Ptr reuseExpiredLease(Lease6Ptr& expired, const Subnet6Ptr& subnet,
                                 const DuidPtr& duid, uint32_t iaid,
                                 const DuidPtr& duid, uint32_t iaid,
+                                const isc::hooks::CalloutHandlePtr& callout_handle,
                                 bool fake_allocation = false);
                                 bool fake_allocation = false);
 
 
     /// @brief a pointer to currently used allocator
     /// @brief a pointer to currently used allocator
@@ -326,6 +337,9 @@ private:
 
 
     /// @brief number of attempts before we give up lease allocation (0=unlimited)
     /// @brief number of attempts before we give up lease allocation (0=unlimited)
     unsigned int attempts_;
     unsigned int attempts_;
+
+    /// @brief hook name index (used in hooks callouts)
+    int hook_index_lease6_select_;
 };
 };
 
 
 }; // namespace isc::dhcp
 }; // namespace isc::dhcp

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

@@ -262,6 +262,7 @@ void CfgMgr::deleteSubnets6() {
     subnets6_.clear();
     subnets6_.clear();
 }
 }
 
 
+
 std::string CfgMgr::getDataDir() {
 std::string CfgMgr::getDataDir() {
     return (datadir_);
     return (datadir_);
 }
 }

+ 12 - 0
src/lib/dhcpsrv/cfgmgr.h

@@ -201,6 +201,18 @@ public:
     /// completely new?
     /// completely new?
     void deleteSubnets6();
     void deleteSubnets6();
 
 
+
+    /// @brief returns const reference to all subnets6
+    ///
+    /// This is used in a hook (subnet6_select), where the hook is able
+    /// to choose a different subnet. Server code has to offer a list
+    /// of possible choices (i.e. all subnets).
+    /// @return a pointer to const Subnet6 collection
+    const Subnet6Collection* getSubnets6() {
+        return (&subnets6_);
+    }
+
+
     /// @brief get IPv4 subnet by address
     /// @brief get IPv4 subnet by address
     ///
     ///
     /// Finds a matching subnet, based on an address. This can be used
     /// Finds a matching subnet, based on an address. This can be used

+ 3 - 0
src/lib/dhcpsrv/dhcpsrv_log.h

@@ -50,6 +50,9 @@ const int DHCPSRV_DBG_TRACE_DETAIL = DBGLVL_TRACE_DETAIL;
 /// Record detailed (and verbose) data on the server.
 /// Record detailed (and verbose) data on the server.
 const int DHCPSRV_DBG_TRACE_DETAIL_DATA = DBGLVL_TRACE_DETAIL_DATA;
 const int DHCPSRV_DBG_TRACE_DETAIL_DATA = DBGLVL_TRACE_DETAIL_DATA;
 
 
+// Trace hook related operations
+const int DHCPSRV_DBG_HOOKS = DBGLVL_TRACE_BASIC;
+
 ///@}
 ///@}
 
 
 
 

+ 6 - 0
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -121,6 +121,12 @@ the database access parameters are changed: in the latter case, the
 server closes the currently open database, and opens a database using
 server closes the currently open database, and opens a database using
 the new parameters.
 the new parameters.
 
 
+% DHCPSRV_HOOK_LEASE6_IA_ADD_SKIP Lease6 (non-temporary) creation was skipped, because of callout skip flag.
+This debug message is printed when a callout installed on lease6_assign
+hook point sets a skip flag. It means that the server was told that no lease6
+should be assigned. The server will not put that lease in its database and the client
+will get a NoAddrsAvail for that IA_NA option.
+
 % DHCPSRV_INVALID_ACCESS invalid database access string: %1
 % DHCPSRV_INVALID_ACCESS invalid database access string: %1
 This is logged when an attempt has been made to parse a database access string
 This is logged when an attempt has been made to parse a database access string
 and the attempt ended in error.  The access string in question - which
 and the attempt ended in error.  The access string in question - which

+ 1 - 0
src/lib/dhcpsrv/tests/Makefile.am

@@ -69,6 +69,7 @@ libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
 libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
 libdhcpsrv_unittests_LDADD += $(GTEST_LDADD)
 libdhcpsrv_unittests_LDADD += $(GTEST_LDADD)
 endif
 endif
 
 

+ 234 - 14
src/lib/dhcpsrv/tests/alloc_engine_unittest.cc

@@ -25,6 +25,10 @@
 
 
 #include <dhcpsrv/tests/test_utils.h>
 #include <dhcpsrv/tests/test_utils.h>
 
 
+#include <hooks/server_hooks.h>
+#include <hooks/callout_manager.h>
+#include <hooks/hooks_manager.h>
+
 #include <boost/shared_ptr.hpp>
 #include <boost/shared_ptr.hpp>
 #include <boost/scoped_ptr.hpp>
 #include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
@@ -37,6 +41,7 @@
 using namespace std;
 using namespace std;
 using namespace isc;
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::asiolink;
+using namespace isc::hooks;
 using namespace isc::dhcp;
 using namespace isc::dhcp;
 using namespace isc::dhcp::test;
 using namespace isc::dhcp::test;
 
 
@@ -105,7 +110,7 @@ public:
         // @todo: check cltt
         // @todo: check cltt
      }
      }
 
 
-    ~AllocEngine6Test() {
+    virtual ~AllocEngine6Test() {
         factory_.destroy();
         factory_.destroy();
     }
     }
 
 
@@ -173,7 +178,7 @@ public:
         // @todo: check cltt
         // @todo: check cltt
      }
      }
 
 
-    ~AllocEngine4Test() {
+    virtual ~AllocEngine4Test() {
         factory_.destroy();
         factory_.destroy();
     }
     }
 
 
@@ -203,7 +208,7 @@ TEST_F(AllocEngine6Test, simpleAlloc6) {
     ASSERT_TRUE(engine);
     ASSERT_TRUE(engine);
 
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
 
     // Check that we got a lease
     // Check that we got a lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
@@ -226,7 +231,7 @@ TEST_F(AllocEngine6Test, fakeAlloc6) {
     ASSERT_TRUE(engine);
     ASSERT_TRUE(engine);
 
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                               true);
+                                               true, CalloutHandlePtr());
 
 
     // Check that we got a lease
     // Check that we got a lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
@@ -248,7 +253,7 @@ TEST_F(AllocEngine6Test, allocWithValidHint6) {
 
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
                                                IOAddress("2001:db8:1::15"),
                                                IOAddress("2001:db8:1::15"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
 
     // Check that we got a lease
     // Check that we got a lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
@@ -286,7 +291,7 @@ TEST_F(AllocEngine6Test, allocWithUsedHint6) {
     // twice.
     // twice.
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
                                                IOAddress("2001:db8:1::1f"),
                                                IOAddress("2001:db8:1::1f"),
-                                               false);
+                                               false, CalloutHandlePtr());
     // Check that we got a lease
     // Check that we got a lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
 
 
@@ -319,7 +324,7 @@ TEST_F(AllocEngine6Test, allocBogusHint6) {
     // with the normal allocation
     // with the normal allocation
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
                                                IOAddress("3000::abc"),
                                                IOAddress("3000::abc"),
-                                               false);
+                                               false, CalloutHandlePtr());
     // Check that we got a lease
     // Check that we got a lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
 
 
@@ -345,12 +350,12 @@ TEST_F(AllocEngine6Test, allocateAddress6Nulls) {
 
 
     // Allocations without subnet are not allowed
     // Allocations without subnet are not allowed
     Lease6Ptr lease = engine->allocateAddress6(Subnet6Ptr(), duid_, iaid_,
     Lease6Ptr lease = engine->allocateAddress6(Subnet6Ptr(), duid_, iaid_,
-                                               IOAddress("::"), false);
+                                               IOAddress("::"), false, CalloutHandlePtr());
     ASSERT_FALSE(lease);
     ASSERT_FALSE(lease);
 
 
     // Allocations without DUID are not allowed either
     // Allocations without DUID are not allowed either
     lease = engine->allocateAddress6(subnet_, DuidPtr(), iaid_,
     lease = engine->allocateAddress6(subnet_, DuidPtr(), iaid_,
-                                     IOAddress("::"), false);
+                                     IOAddress("::"), false, CalloutHandlePtr());
     ASSERT_FALSE(lease);
     ASSERT_FALSE(lease);
 }
 }
 
 
@@ -439,7 +444,7 @@ TEST_F(AllocEngine6Test, smallPool6) {
     cfg_mgr.addSubnet6(subnet_);
     cfg_mgr.addSubnet6(subnet_);
 
 
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
     Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                               false);
+                                               false, CalloutHandlePtr());
 
 
     // Check that we got that single lease
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
@@ -485,7 +490,7 @@ TEST_F(AllocEngine6Test, outOfAddresses6) {
     // There is just a single address in the pool and allocated it to someone
     // There is just a single address in the pool and allocated it to someone
     // else, so the allocation should fail
     // else, so the allocation should fail
     Lease6Ptr lease2 = engine->allocateAddress6(subnet_, duid_, iaid_,
     Lease6Ptr lease2 = engine->allocateAddress6(subnet_, duid_, iaid_,
-                                                IOAddress("::"), false);
+                                                IOAddress("::"), false, CalloutHandlePtr());
     EXPECT_FALSE(lease2);
     EXPECT_FALSE(lease2);
 }
 }
 
 
@@ -519,7 +524,7 @@ TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
 
 
     // CASE 1: Asking for any address
     // CASE 1: Asking for any address
     lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
     lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
-                                     true);
+                                     true, CalloutHandlePtr());
     // Check that we got that single lease
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
@@ -529,7 +534,7 @@ TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
 
 
     // CASE 2: Asking specifically for this address
     // CASE 2: Asking specifically for this address
     lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress(addr.toText()),
     lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress(addr.toText()),
-                                     true);
+                                     true, CalloutHandlePtr());
     // Check that we got that single lease
     // Check that we got that single lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
     EXPECT_EQ(addr.toText(), lease->addr_.toText());
@@ -563,7 +568,8 @@ TEST_F(AllocEngine6Test, requestReuseExpiredLease6) {
 
 
     // A client comes along, asking specifically for this address
     // A client comes along, asking specifically for this address
     lease = engine->allocateAddress6(subnet_, duid_, iaid_,
     lease = engine->allocateAddress6(subnet_, duid_, iaid_,
-                                     IOAddress(addr.toText()), false);
+                                     IOAddress(addr.toText()), false,
+                                     CalloutHandlePtr());
 
 
     // Check that he got that single lease
     // Check that he got that single lease
     ASSERT_TRUE(lease);
     ASSERT_TRUE(lease);
@@ -1020,4 +1026,218 @@ TEST_F(AllocEngine4Test, renewLease4) {
     detailCompareLease(lease, from_mgr);
     detailCompareLease(lease, from_mgr);
 }
 }
 
 
+/// @brief helper class used in Hooks testing in AllocEngine6
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+class HookAllocEngine6Test : public AllocEngine6Test {
+public:
+    HookAllocEngine6Test() {
+        resetCalloutBuffers();
+    }
+
+    virtual ~HookAllocEngine6Test() {
+        HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+            "lease6_select");
+    }
+
+    /// @brief clears out buffers, so callouts can store received arguments
+    void resetCalloutBuffers() {
+        callback_name_ = string("");
+        callback_subnet6_.reset();
+        callback_fake_allocation_ = false;
+        callback_lease6_.reset();
+        callback_argument_names_.clear();
+        callback_addr_original_ = IOAddress("::");
+        callback_addr_updated_ = IOAddress("::");
+    }
+
+    /// callback that stores received callout name and received values
+    static int
+    lease6_select_callout(CalloutHandle& callout_handle) {
+
+        callback_name_ = string("lease6_select");
+
+        callout_handle.getArgument("subnet6", callback_subnet6_);
+        callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+        callout_handle.getArgument("lease6", callback_lease6_);
+
+        callback_addr_original_ = callback_lease6_->addr_;
+
+        callback_argument_names_ = callout_handle.getArgumentNames();
+        return (0);
+    }
+
+    /// callback that overrides the lease with different values
+    static int
+    lease6_select_different_callout(CalloutHandle& callout_handle) {
+
+        // Let's call the basic callout, so it can record all parameters
+        lease6_select_callout(callout_handle);
+
+        // Now we need to tweak the least a bit
+        Lease6Ptr lease;
+        callout_handle.getArgument("lease6", lease);
+        callback_addr_updated_ = addr_override_;
+        lease->addr_ = callback_addr_updated_;
+        lease->t1_ = t1_override_;
+        lease->t2_ = t2_override_;
+        lease->preferred_lft_ = pref_override_;
+        lease->valid_lft_ = valid_override_;
+
+        return (0);
+    }
+
+    // Values to be used in callout to override lease6 content
+    static const IOAddress addr_override_;
+    static const uint32_t t1_override_;
+    static const uint32_t t2_override_;
+    static const uint32_t pref_override_;
+    static const uint32_t valid_override_;
+
+    // Callback will store original and overridden values here
+    static IOAddress callback_addr_original_;
+    static IOAddress callback_addr_updated_;
+
+    // Buffers (callback will store received values here)
+    static string callback_name_;
+    static Subnet6Ptr callback_subnet6_;
+    static Lease6Ptr callback_lease6_;
+    static bool callback_fake_allocation_;
+    static vector<string> callback_argument_names_;
+};
+
+// For some reason intialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine6Test::addr_override_("2001:db8::abcd");
+const uint32_t HookAllocEngine6Test::t1_override_ = 6000;
+const uint32_t HookAllocEngine6Test::t2_override_ = 7000;
+const uint32_t HookAllocEngine6Test::pref_override_ = 8000;
+const uint32_t HookAllocEngine6Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine6Test::callback_addr_original_("::");
+IOAddress HookAllocEngine6Test::callback_addr_updated_("::");
+
+string HookAllocEngine6Test::callback_name_;
+Subnet6Ptr HookAllocEngine6Test::callback_subnet6_;
+Lease6Ptr HookAllocEngine6Test::callback_lease6_;
+bool HookAllocEngine6Test::callback_fake_allocation_;
+vector<string> HookAllocEngine6Test::callback_argument_names_;
+
+// This test checks if the lease6_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine6Test, lease6_select) {
+
+    // Note: The following order is working as expected:
+    // 1. create AllocEngine (that register hook points)
+    // 2. call loadLibraries()
+    //
+    // This order, however, causes segfault in HooksManager
+    // 1. call loadLibraries()
+    // 2. create AllocEngine (that register hook points)
+
+    // Create allocation engine (hook names are registered in its ctor)
+    boost::scoped_ptr<AllocEngine> engine;
+    ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+    ASSERT_TRUE(engine);
+
+    // Initialize Hooks Manager
+    vector<string> libraries; // no libraries at this time
+    HooksManager::getHooksManager().loadLibraries(libraries);
+
+    // Install pkt6_receive_callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "lease6_select", lease6_select_callout));
+
+    CalloutHandlePtr callout_handle = HooksManager::getHooksManager().createCalloutHandle();
+
+    Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
+                                               false, callout_handle);
+    // Check that we got a lease
+    ASSERT_TRUE(lease);
+
+    // Do all checks on the lease
+    checkLease6(lease);
+
+    // Check that the lease is indeed in LeaseMgr
+    Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
+    ASSERT_TRUE(from_mgr);
+
+    // Check that callouts were indeed called
+    EXPECT_EQ("lease6_select", callback_name_);
+
+    // Now check that the lease in LeaseMgr has the same parameters
+    ASSERT_TRUE(callback_lease6_);
+    detailCompareLease(callback_lease6_, from_mgr);
+
+    ASSERT_TRUE(callback_subnet6_);
+    EXPECT_EQ(subnet_->toText(), callback_subnet6_->toText());
+
+    EXPECT_EQ(callback_fake_allocation_, false);
+
+    // Check if all expected parameters are reported. It's a bit tricky, because
+    // order may be different. If the test starts failing, because someone tweaked
+    // hooks engine, we'll have to implement proper vector matching (ignoring order)
+    vector<string> expected_argument_names;
+    expected_argument_names.push_back("fake_allocation");
+    expected_argument_names.push_back("lease6");
+    expected_argument_names.push_back("subnet6");
+    EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+}
+
+// This test checks if lease6_select callout is able to override the values
+// in a lease6.
+TEST_F(HookAllocEngine6Test, change_lease6_select) {
+
+    // Make sure that the overridden values are different than the ones from
+    // subnet originally used to create the lease
+    ASSERT_NE(t1_override_, subnet_->getT1());
+    ASSERT_NE(t2_override_, subnet_->getT2());
+    ASSERT_NE(pref_override_, subnet_->getPreferred());
+    ASSERT_NE(valid_override_, subnet_->getValid());
+    ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+    // Create allocation engine (hook names are registered in its ctor)
+    boost::scoped_ptr<AllocEngine> engine;
+    ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+    ASSERT_TRUE(engine);
+
+    // Initialize Hooks Manager
+    vector<string> libraries; // no libraries at this time
+    HooksManager::getHooksManager().loadLibraries(libraries);
+
+    // Install a callout
+    EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        "lease6_select", lease6_select_different_callout));
+
+    // Normally, dhcpv6_srv would passed the handle when calling allocateAddress6,
+    // but in tests we need to create it on our own.
+    CalloutHandlePtr callout_handle = HooksManager::getHooksManager().createCalloutHandle();
+
+    // Call allocateAddress6. Callouts should be triggered here.
+    Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
+                                               false, callout_handle);
+    // Check that we got a lease
+    ASSERT_TRUE(lease);
+
+    // See if the values overridden by callout are there
+    EXPECT_TRUE(lease->addr_.equals(addr_override_));
+    EXPECT_EQ(t1_override_, lease->t1_);
+    EXPECT_EQ(t2_override_, lease->t2_);
+    EXPECT_EQ(pref_override_, lease->preferred_lft_);
+    EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+    // Now check if the lease is in the database
+    Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
+    ASSERT_TRUE(from_mgr);
+
+    // Check if values in the database are overridden
+    EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+    EXPECT_EQ(t1_override_, from_mgr->t1_);
+    EXPECT_EQ(t2_override_, from_mgr->t2_);
+    EXPECT_EQ(pref_override_, from_mgr->preferred_lft_);
+    EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+}
+
 }; // End of anonymous namespace
 }; // End of anonymous namespace

+ 8 - 2
src/lib/hooks/server_hooks.cc

@@ -55,7 +55,8 @@ ServerHooks::registerHook(const string& name) {
     inverse_hooks_[index] = name;
     inverse_hooks_[index] = name;
 
 
     // Log it if debug is enabled
     // Log it if debug is enabled
-    LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_HOOK_REGISTERED).arg(name);
+    /// @todo See todo comment in reset() below.
+    //LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_HOOK_REGISTERED).arg(name);
 
 
     // ... and return numeric index.
     // ... and return numeric index.
     return (index);
     return (index);
@@ -85,7 +86,12 @@ ServerHooks::reset() {
 
 
     // Log a warning - although this is done during testing, it should never be
     // Log a warning - although this is done during testing, it should never be
     // seen in a production system.
     // seen in a production system.
-    LOG_WARN(hooks_logger, HOOKS_HOOK_LIST_RESET);
+    /// @todo Implement proper workaround here. The issue is when the first
+    /// module (e.g. Dhcp6Srv module) initializes global structure it calls
+    /// HooksManager::registerHooks() which in turn creates ServerHooks object.
+    /// Its constructor calls reset() method, but the loggers are not initialized
+    /// yet and exception is thrown.
+    //LOG_WARN(hooks_logger, HOOKS_HOOK_LIST_RESET);
 }
 }
 
 
 // Find the name associated with a hook index.
 // Find the name associated with a hook index.