Kea  0.9-git
Hooks Developer's Guide

Introduction

Although the Kea framework and its DHCP programs provide comprehensive functionality, there will be times when it does not quite do what you require: the processing has to be extended in some way to solve your problem.

Since the Kea source code is freely available (Kea being an open-source project), one option is to modify it to do what you want. Whilst perfectly feasible, there are drawbacks:

  • Although well-documented, Kea is a large program. Just understanding how it works will take a significant amount of time. In addition, despite the fact that its object-oriented design keeps the coupling between modules to a minimum, an inappropriate change to one part of the program during the extension could cause another to behave oddly or to stop working altogether.
  • The change may need to be re-applied or re-written with every new version of Kea. As new functionality is added or bugs are fixed, the code or algorithms in the core software may change - and may change significantly.

To overcome these problems, Kea provides the "Hooks" interface - a defined interface for third-party or user-written code. (For ease of reference in the rest of this document, all such code will be referred to as "user code".) At specific points in its processing ("hook points") Kea will make a call to this code. The call passes data that the user code can examine and, if required, modify. Kea uses the modified data in the remainder of its processing.

In order to minimise the interaction between Kea and the user code, the latter is built independently of Kea in the form of a shared library (or libraries). These are made known to Kea through its configuration mechanism, and Kea loads the library at run time. Libraries can be unloaded and reloaded as needed while Kea is running.

Use of a defined API and the Kea configuration mechanism means that as new versions of Kea are released, there is no need to modify the user code. Unless there is a major change in an interface (which will be clearly documented), all that will be required is a rebuild of the libraries.

Note
Although the defined interface should not change, the internals of some of the classes and structures referenced by the user code may change between versions of Kea. These changes have to be reflected in the compiled version of the software, hence the need for a rebuild.

Languages

The core of Kea is written in C++. While it is the intention to provide interfaces into user code written in other languages, the initial versions of the Hooks system requires that user code be written in C++. All examples in this guide are in that language.

Terminology

In the remainder of this guide, the following terminology is used:

  • Hook/Hook Point - used interchageably, this is a point in the code at which a call to user functions is made. Each hook has a name and each hook can have any number (including 0) of user functions attached to it.
  • Callout - a user function called by the server at a hook point. This is so-named because the server "calls out" to the library to execute a user function.
  • Framework function - the functions that a user library needs to supply in order for the hooks framework to load and unload the library.
  • User code/user library - non-Kea code that is compiled into a shared library and loaded by Kea into its address space.

Tutorial

To illustrate how to write code that integrates with Kea, we will use the following (rather contrived) example:

The Kea DHCPv4 server is used to allocate IPv4 addresses to clients (as well as to pass them other information such as the address of DNS servers). We will suppose that we need to classify clients requesting IPv4 addresses according to their hardware address, and want to log both the hardware address and allocated IP address for the clients of interest.

The following sections describe how to implement these requirements. The code presented here is not efficient and there are better ways of doing the task. The aim however, is to illustrate the main features of user hook code not to provide an optimal solution.

Framework Functions

Loading and initializing a library holding user code makes use of three (user-supplied) functions:

  • version - defines the version of Kea code with which the user-library is built
  • load - called when the library is loaded by the server.
  • unload - called when the library is unloaded by the server.

Of these, only "version" is mandatory, although in our example, all three are used.

The "version" Function

"version" is used by the hooks framework to check that the libraries it is loading are compatible with the version of Kea being run. Although the hooks system allows Kea and user code to interface through a defined API, the relationship is somewhat tight in that the user code will depend on the internal structures of Kea. If these change - as they can between Kea releases - and Kea is run with a version of user code built against an earlier version of Kea, a program crash could result.

To guard against this, the "version" function must be provided in every library. It returns a constant defined in header files of the version of Kea against which it was built. The hooks framework checks this for compatibility with the running version of Kea before loading the library.

In this tutorial, we'll put "version" in its own file, version.cc. The contents are:

// version.cc
#include <hooks/hooks.h>
extern "C" {
int version() {
return (KEA_HOOKS_VERSION);
}
}

The file "hooks/hooks.h" is specified relative to the Kea libraries source directory - this is covered later in the section Building the Library. It defines the symbol KEA_HOOKS_VERSION, which has a value that changes on every release of Kea: this is the value that needs to be returned to the hooks framework.

A final point to note is that the definition of "version" is enclosed within 'extern "C"' braces. All functions accessed by the hooks framework use C linkage, mainly to avoid the name mangling that accompanies use of the C++ compiler, but also to avoid issues related to namespaces.

The "load" and "unload" Functions

As the names suggest, "load" is called when a library is loaded and "unload" called when it is unloaded. (It is always guaranteed that "load" is called: "unload" may not be called in some circumstances, e.g. if the system shuts down abnormally.) These functions are the places where any library-wide resources are allocated and deallocated. "load" is also the place where any callouts with non-standard names (names that are not hook point names) can be registered: this is covered further in the section Registering Callouts.

The example does not make any use callouts with non-standard names. However, as our design requires that the log file be open while Kea is active and the library loaded, we'll open the file in the "load" function and close it in "unload".

We create two files, one for the file handle declaration:

// library_common.h
#ifndef LIBRARY_COMMON_H
#define LIBRARY_COMMON_H
#include <fstream>
// "Interesting clients" log file handle declaration.
extern std::fstream interesting;
#endif // LIBRARY_COMMON_H

... and one to hold the "load" and "unload" functions:

// load_unload.cc
#include <hooks/hooks.h>
#include "library_common.h"
using namespace isc::hooks;
// "Interesting clients" log file handle definition.
std::fstream interesting;
extern "C" {
interesting.open("/data/clients/interesting.log",
std::fstream::out | std::fstream::app);
return (interesting ? 0 : 1);
}
int unload() {
if (interesting) {
interesting.close();
}
return (0);
}
}

Notes:

  • The file handle ("interesting") is declared in a header file and defined outside of any function. This means it can be accessed by any function within the user library. For convenience, the definition is in the load_unload.cc file.
  • "load" is called with a LibraryHandle argument, this being used in the registration of functions. As no functions are being registered in this example, the argument specification omits the variable name (whilst retaining the type) to avoid an "unused variable" compiler warning. (The LibraryHandle and its use is discussed in the section The LibraryHandle Object.)
  • In the current version of the hooks framework, it is not possible to pass any configuration information to the "load" function. The name of the log file must therefore be hard-coded as an absolute path name or communicated to the user code by some other means.
  • "load" must 0 on success and non-zero on error. The hooks framework will abandon the loading of the library if "load" returns an error status. (In this example, "interesting" can be tested as a boolean value, returning "true" if the file opened successfully.)
  • "unload" closes the log file if it is open and is a no-op otherwise. As with "load", a zero value must be returned on success and a non-zero value on an error. The hooks framework will record a non-zero status return as an error in the current Kea log but otherwise ignore it.
  • As before, the function definitions are enclosed in 'extern "C"' braces.

Callouts

Having sorted out the framework, we now come to the functions that actually do something. These functions are known as "callouts" because the Kea code "calls out" to them. Each Kea server has a number of hooks to which callouts can be attached: server-specific documentation describes in detail the points in the server at which the hooks are present together with the data passed to callouts attached to them.

Before we continue with the example, we'll discuss how arguments are passed to callouts and information is returned to the server. We will also discuss how information can be moved between callouts.

The Callout Signature

All callouts are declared with the signature:

extern "C" {
int callout(CalloutHandle& handle);
}

(As before, the callout is declared with "C" linkage.) Information is passed between Kea and the callout through name/value pairs in the CalloutHandle object. The object is also used to pass information between callouts on a per-request basis. (Both of these concepts are explained below.)

A callout returns an "int" as a status return. A value of 0 indicates success, anything else signifies an error. The status return has no effect on server processing; the only difference between a success and error code is that if the latter is returned, the server will log an error, specifying both the library and hook that generated it. Effectively the return status provides a quick way for a callout to log error information to the Kea logging system.

Callout Arguments

The CalloutHandle object provides two methods to get and set the arguments passed to the callout. These methods are called (naturally enough) getArgument and SetArgument. Their usage is illustrated by the following code snippets.

// Server-side code snippet to show the setting of arguments
int count = 10;
boost::shared_ptr<Pkt4> pktptr = ... // Set to appropriate value
// Assume that "handle" has been created
handle.setArgument("data_count", count);
handle.setArgument("inpacket", pktptr);
// Call the callouts attached to the hook
...
// Retrieve the modified values
handle.getArgument("data_count", count);
handle.getArgument("inpacket", pktptr);

In the callout

int number;
boost::shared_ptr<Pkt4> packet;
// Retrieve data set by the server.
handle.getArgument("data_count", number);
handle.getArgument("inpacket", packet);
// Modify "number"
number = ...;
// Update the arguments to send the value back to the server.
handle.setArgument("data_count", number);

As can be seen "getArgument" is used to retrieve data from the CalloutHandle, and setArgument used to put data into it. If a callout wishes to alter data and pass it back to the server, it should retrieve the data with getArgument, modify it, and call setArgument to send it back.

There are several points to be aware of:

  • the data type of the variable in the call to getArgument must match the data type of the variable passed to the corresponding setArgument exactly: using what would normally be considered to be a "compatible" type is not enough. For example, if the server passed an argument as an "int" and the callout attempted to retrieve it as a "long", an exception would be thrown even though any value that can be stored in an "int" will fit into a "long". This restriction also applies the "const" attribute but only as applied to data pointed to by pointers, e.g. if an argument is defined as a "char*", an exception will be thrown if an attempt is made to retrieve it into a variable of type "const char*". (However, if an argument is set as a "const int", it can be retrieved into an "int".) The documentation of each hook point will detail the data type of each argument.
  • Although all arguments can be modified, some altered values may not be read by the server. (These would be ones that the server considers "read-only".) Consult the documentation of each hook to see whether an argument can be used to transfer data back to the server.
  • If a pointer to an object is passed to a callout (either a "raw" pointer, or a boost smart pointer (as in the example above), and the underlying object is altered through that pointer, the change will be reflected in the server even if no call is made to setArgument.

In all cases, consult the documentation for the particular hook to see whether parameters can be modified. As a general rule:

  • Do not alter arguments unless you mean the change to be reflected in the server.
  • If you alter an argument, call CalloutHandle::setArgument to update the value in the CalloutHandle object.

The "Skip" Flag

When a to callouts attached to a hook returns, the server will usually continue its processing. However, a callout might have done something that means that the server should follow another path. Possible actions a server could take include:

  • Skip the next stage of processing because the callout has already done it. For example, a hook is located just before the DHCP server allocates an address to the client. A callout may decide to allocate special addresses for certain clients, in which case it needs to tell the server not to allocate an address in this case.
  • Drop the packet and continue with the next request. A possible scenario is a server where a callout inspects the hardware address of the client sending the packet and compares it against a black list; if the address is on it, the callout notifies the server to drop the packet.

To handle these common cases, the CalloutHandle has a "skip" flag. This is set by a callout when it wishes the server to skip normal processing. It is set false by the hooks framework before callouts on a hook are called. If the flag is set on return, the server will take the "skip" action relevant for the hook.

The methods to get and set the "skip" flag are getSkip and setSkip. Their usage is intuitive:

// Get the current setting of the skip flag.
bool skip = handle.getSkip();
// Do some processing...
:
if (lease_allocated) {
// Flag the server to skip the next step of the processing as we
// already have an address.
handle.setSkip(true);
}
return;

Like arguments, the "skip" flag is passed to all callouts on a hook. Callouts later in the list are able to examine (and modify) the settings of earlier ones.

Per-Request Context

Although the Kea modules can be characterised as handling a single packet at a time - e.g. the DHCPv4 server receives a DHCPDISCOVER packet, processes it and responds with an DHCPOFFER, this may not always be true. Future developments may have the server processing multiple packets simultaneously, or to suspend processing on a packet and resume it at a later time after other packets have been processed.

As well as argument information, the CalloutHandle object can be used by callouts to attach information to a packet being handled by the server. This information (known as "context") is not used by the server: its purpose is to allow callouts to pass information between one another on a per-packet basis.

Context associated with a packet only exists only for the duration of the processing of that packet: when processing is completed, the context is destroyed. A new packet starts with a new (empty) context. Context is particularly useful in servers that may be processing multiple packets simultaneously: callouts can effectively attach data to a packet that follows the packet around the system.

Context information is held as name/value pairs in the same way as arguments, being accessed by the pair of methods setContext and getContext. They have the same restrictions as the setArgument and getArgument methods - the type of data retrieved from context must exactly match the type of the data set.

The example in the next section illustrates their use.

Example Callouts

Continuing with the tutorial, the requirements need us to retrieve the hardware address of the incoming packet, classify it, and write it, together with the assigned IP address, to a log file. Although we could do this in one callout, for this example we'll use two:

  • pkt4_receive - a callout on this hook is invoked when a packet has been received and has been parsed. It is passed a single argument, "query4" which is an isc::dhcp::Pkt4 object (representing a DHCP v4 packet). We will do the classification here.
  • pkt4_send - called when a response is just about to be sent back to the client. It is passed a single argument "response4". This is the point at which the example code will write the hardware and IP addresses to the log file.

The standard for naming callouts is to give them the same name as the hook. If this is done, the callouts will be automatically found by the Hooks system (this is discussed further in section Registering Callouts). For our example, we will assume this is the case, so the code for the first callout (used to classify the client's hardware address) is:

// pkt_receive4.cc
#include <hooks/hooks.h>
#include <dhcp/pkt4.h>
#include "library_common.h"
#include <string>
using namespace isc::dhcp;
using namespace isc::hooks;
using namespace std;
extern "C" {
// This callout is called at the "pkt4_receive" hook.
// A pointer to the packet is passed to the callout via a "boost" smart
// pointer. The include file "pkt4.h" typedefs a pointer to the Pkt4
// object as Pkt4Ptr. Retrieve a pointer to the object.
Pkt4Ptr query4_ptr;
handle.getArgument("query4", query4_ptr);
// Point to the hardware address.
HWAddrPtr hwaddr_ptr = query4_ptr->getHWAddr();
// The hardware address is held in a public member variable. We'll classify
// it as interesting if the sum of all the bytes in it is divisible by 4.
// (This is a contrived example after all!)
long sum = 0;
for (int i = 0; i < hwaddr_ptr->hwaddr_.size(); ++i) {
sum += hwaddr_ptr->hwaddr_[i];
}
// Classify it.
if (sum % 4 == 0) {
// Store the text form of the hardware address in the context to pass
// to the next callout.
string hwaddr = hwaddr_ptr->toText();
handle.setContext("hwaddr", hwaddr);
}
return (0);
};
}

The pkt4_receive callout placed the hardware address of an interesting client in the "hwaddr" context for the packet. Turning now to the callout that will write this information to the log file:

// pkt4_send.cc
#include <hooks/hooks.h>
#include <dhcp/pkt4.h>
#include "library_common.h"
#include <string>
using namespace isc::dhcp;
using namespace isc::hooks;
using namespace std;
extern "C" {
// This callout is called at the "pkt4_send" hook.
int pkt4_send(CalloutHandle& handle) {
// Obtain the hardware address of the "interesting" client. We have to
// use a try...catch block here because if the client was not interesting,
// no information would be set and getArgument would thrown an exception.
string hwaddr;
try {
handle.getContext("hwaddr", hwaddr);
// getContext didn't throw so the client is interesting. Get a pointer
// to the reply.
Pkt4Ptr response4_ptr;
handle.getArgument("response4", response4_ptr);
// Get the string form of the IP address.
string ipaddr = response4_ptr->getYiaddr().toText();
// Write the information to the log file.
interesting << hwaddr << " " << ipaddr << "\n";
// ... and to guard against a crash, we'll flush the output stream.
flush(interesting);
} catch (const NoSuchCalloutContext&) {
// No such element in the per-request context with the name "hwaddr".
// This means that the request was not an interesting, so do nothing
// and dismiss the exception.
}
return (0);
}
}

Building the Library

Building the code requires building a shareable library. This requires the the code be compiled as positition-independent code (using the compiler's "-fpic" switch) and linked as a shared library (with the linker's "-shared" switch). The build command also needs to point to the Kea include directory and link in the appropriate libraries.

Assuming that Kea has been installed in the default location, the command line needed to create the library using the Gnu C++ compiler on a Linux system is:

g++ -I /usr/include/kea -L /usr/lib/kea/lib -fpic -shared -o example.so \ load_unload.cc pkt4_receive.cc pkt4_send.cc version.cc \
-lkea-dhcpsrv -lkea-dhcp++ -lkea-hooks -lkea-log -lkea-util -lkea-exceptions

Notes:

  • The compilation command and switches required may vary depending on your operating system and compiler - consult the relevant documentation for details.
  • The values for the "-I" and "-L" switches depend on where you have installed Kea.
  • The list of libraries that need to be included in the command line depends on the functionality used by the hook code and the module to which they are attached. Depending on operating system, you may also need to explicitly list libraries on which the Kea libraries you link against depend.

Configuring the Hooks Library

The final step is to make the library known to Kea. The configuration keywords of all Kea modules to which hooks can be added contain the "hooks-libraries" element and user libraries are added to this. (The Kea hooks system can handle multiple libraries - this is discussed below.)

To add the example library (assumed to be in /usr/local/lib) to the DHCPv4 module, it must be listed in the "hooks-libraries" element of the "Dhcp4" part of the configuration file:

"Dhcp4": {
:
"hooks-libraries": [ "/usr/local/lib/example.so" ]
:
}

(Note that "hooks" is plural.)

The DHCPv4 server will load the library and execute the callouts each time a request is received.

Note
The above assumes that the hooks library will be used with a version of Kea that is dynamically-linked. For information regarding running hooks libraries against a statically-linked Kea, see Running Against a Statically-Linked Kea.

Advanced Topics

Context Creation and Destruction

As well as the hooks defined by the server, the hooks framework defines two hooks of its own, "context_create" and "context_destroy". The first is called when a request is created in the server, before any of the server-specific hooks gets called. It's purpose it to allow a library to initialize per-request context. The second is called after all server-defined hooks have been processed, and is to allow a library to tidy up.

As an example, the pkt4_send example above required that the code check for an exception being thrown when accessing the "hwaddr" context item in case it was not set. An alternative strategy would have been to provide a callout for the "context_create" hook and set the context item "hwaddr" to an empty string. Instead of needing to handle an exception, pkt4_send would be guaranteed to get something when looking for the hwaddr item and so could write or not write the output depending on the value.

In most cases, "context_destroy" is not needed as the Hooks system automatically deletes context. An example where it could be required is where memory has been allocated by a callout during the processing of a request and a raw pointer to it stored in the context object. On destruction of the context, that memory will not be automatically released. Freeing in the memory in the "context_destroy callout will solve that problem.

Actually, when the context is destroyed, the destructor associated with any objects stored in it are run. Rather than point to allocated memory with a raw pointer, a better idea would be to point to it with a boost "smart" pointer and store that pointer in the context. When the context is destroyed, the smart pointer's destructor is run, which will automatically delete the pointed-to object.

These approaches are illustrated in the following examples. Here it is assumed that the hooks library is performing some form of security checking on the packet and needs to maintain information in a user-specified "SecurityInformation" object. (The details of this fictitious object are of no concern here.) The object is created in the context_create callout and used in both the pkt4_receive and the pkt4_send callouts.

// Storing information in a "raw" pointer. Assume that the
#include <hooks/hooks.h>
:
extern "C" {
// context_create callout - called when the request is created.
int context_create(CalloutHandle& handle) {
// Create the security information and store it in the context
// for this packet.
SecurityInformation* si = new SecurityInformation();
handle.setContext("security_information", si);
}
// Callouts that use the context
int pkt4_receive(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
SecurityInformation si;
handle.getContext("security_information", si);
:
:
// Set the security information
si->setSomething(...);
// The pointed-to information has been updated but the pointer has not been
// altered, so there is no need to call setContext() again.
}
int pkt4_send(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
SecurityInformation si;
handle.getContext("security_information", si);
:
:
// Retrieve security information
bool active = si->getSomething(...);
:
}
// Context destruction. We need to delete the pointed-to SecurityInformation
// object because we will lose the pointer to it when the CalloutHandle is
// destroyed.
int context_destroy(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
SecurityInformation si;
handle.getContext("security_information", si);
// Delete the pointed-to memory.
delete si;
}

The requirement for the context_destroy callout can be eliminated if a Boost shared ptr is used to point to the allocated memory:

// Storing information in a "raw" pointer. Assume that the
#include <hooks/hooks.h>
#include <boost/shared_ptr.hpp>
:
extern "C" {
// context_create callout - called when the request is created.
int context_create(CalloutHandle& handle) {
// Create the security information and store it in the context for this
// packet.
boost::shared_ptr<SecurityInformation> si(new SecurityInformation());
handle.setContext("security_information", si);
}
// Other than the data type, a shared pointer has similar semantics to a "raw"
// pointer. Only the code from pkt4_receive is shown here.
int pkt4_receive(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
boost::shared_ptr<SecurityInformation> si;
handle.setContext("security_information", si);
:
:
// Modify the security information
si->setSomething(...);
// The pointed-to information has been updated but the pointer has not
// altered, so theree is no need to reset the context.
}
// No context_destroy callout is needed to delete the allocated
// SecurityInformation object. When the CalloutHandle is destroyed, the shared
// pointer object will be destroyed. If that is the last shared pointer to the
// allocated memory, then it too will be deleted.

(Note that a Boost shared pointer - rather than any other Boost smart pointer - should be used, as the pointer objects are copied within the hooks framework and only shared pointers have the correct behavior for the copy operation.)

Registering Callouts

As briefly mentioned in Example Callouts, the standard is for callouts in the user library to have the same name as the name of the hook to which they are being attached. This convention was followed in the tutorial, e.g. the callout that needed to be attached to the "pkt4_receive" hook was named pkt4_receive.

The reason for this convention is that when the library is loaded, the hook framework automatically searches the library for functions with the same names as the server hooks. When it finds one, it attaches it to the appropriate hook point. This simplifies the loading process and bookkeeping required to create a library of callouts.

However, the hooks system is flexible in this area: callouts can have non-standard names, and multiple callouts can be registered on a hook.

The LibraryHandle Object

The way into the part of the hooks framework that allows callout registration is through the LibraryHandle object. This was briefly introduced in the discussion of the framework functions, in that an object of this type is pass to the "load" function. A LibraryHandle can also be obtained from within a callout by calling the CalloutHandle's getLibraryHandle() method.

The LibraryHandle provides three methods to manipulate callouts:

  • registerCallout - register a callout on a hook.
  • deregisterCallout - deregister a callout from a hook.
  • deregisterAllCallouts - deregister all callouts on a hook.

The following sections cover some of the ways in which these can be used.

Non-Standard Callout Names

The example in the tutorial used standard names for the callouts. As noted above, it is possible to use non-standard names. Suppose, instead of the callout names "pkt4_receive" and "pkt4_send", we had named our callouts "classify" and "write_data". The hooks framework would not have registered these callouts, so we would have needed to do it ourself. The place to do this is the "load" framework function, and its code would have had to been modified to:

int load(LibraryHandle& libhandle) {
// Register the callouts on the hooks. We assume that a header file
// declares the "classify" and "write_data" functions.
libhandle.registerCallout("pkt4_receive", classify);
libhandle.registerCallout("pkt4_send", write_data);
// Open the log file
interesting.open("/data/clients/interesting.log",
std::fstream::out | std::fstream::app);
return (interesting ? 0 : 1);
}

It is possible for a library to contain callouts with both standard and non-standard names: ones with standard names will be registered automatically, ones with non-standard names need to be registered manually.

Multiple Callouts on a Hook

The Kea hooks framework allows multiple callouts to be attached to a hook point. Although it is likely to be rare for user code to need to do this, there may be instances where it make sense.

To register multiple callouts on a hook, just call LibraryHandle::registerCallout multiple times on the same hook, e.g.

libhandle.registerCallout("pkt4_receive", classify);
libhandle.registerCallout("pkt4_receive", write_data);

The hooks framework will call the callouts in the order they are registered. The same CalloutHandle is passed between them, so any change made to the CalloutHandle's arguments, "skip" flag, or per-request context by the first is visible to the second.

Dynamic Registration and Reregistration of Callouts

The previous sections have dealt with callouts being registered during the call to "load". The hooks framework is more flexible than that in that callouts can be registered and deregistered within a callout. In fact, a callout is able to register or deregister itself, and a callout is able to be registered on a hook multiple times.

Using our contrived example again, the DHCPv4 server processes one request to completion before it starts processing the next. With this knowledge, we could alter the logic of the code so that the callout attached to the "pkt4_receive" hook registers the callout doing the logging when it detects an interesting packet, and the callout doing the logging deregisters itself in its execution. The relevant modifications to the code in the tutorial are shown below:

// pkt4_receive.cc
// :
int pkt4_receive(CalloutHandle& handle) {
:
:
// Classify it.
if (sum % 4 == 0) {
// Store the text form of the hardware address in the context to pass
// to the next callout.
handle.setContext("hwaddr", hwaddr_ptr->hwaddr_.toText());
// Register the callback to log the data.
handle.getLibraryHandle().registerCallout("pkt4_send", write_data);
}
return (0);
};
// pkt4_send.cc
:
int write_data(CalloutHandle& handle) {
// Obtain the hardware address of the "interesting" client. As the
// callback is only registered when interesting data is present, we
// know that the context contains the hardware address so an exception
// will not be thrown when we call getArgument().
string hwaddr;
handle.getArgument("hwaddr", hwaddr);
// The pointer to the reply.
ConstPkt4Ptr reply;
handle.getArgument("reply", reply);
// Get the string form of the IP address.
string ipaddr = reply->getYiaddr().toText():
// Write the information to the log file and flush.
interesting << hwaddr << " " << ipaddr << "\n";
flush(interesting);
// We've logged the data, so deregister ourself. This callout will not
// be called again until it is registered by pkt4_receive.
handle.getLibraryHandle().deregisterCallout("pkt4_send", write_data);
return (0);
}

Note that the above example used a non-standard name for the callout that wrote the data. Had the name been a standard one, it would have been registered when the library was loaded and called for the first request, regardless of whether that was defined as "interesting". (Although as callouts with standard names are always registered before "load" gets called, we could have got round that problem by deregistering that particular callout in the "load" function.)

Note
Deregistration of a callout on the hook that is currently being called only takes effect when the server next calls the hook. To illustrate this, suppose the callouts attached to a hook are A, B and C (in that order), and during execution, A deregisters B and C and adds D. When callout A returns, B and C will still run. The next time the server calls the hook's callouts, A and D will run (in that order).

Multiple User Libraries

As alluded to in the section Configuring the Hooks Library, Kea can load multiple libraries. The libraries are loaded in the order specified in the configuration, and the callouts attached to the hooks in the order presented by the libraries.

The following picture illustrates this, and also illustrates the scope of data passed around the system.

DataScopeArgument.png
Scope of Arguments

In this illustration, a server has three hook points, alpha, beta and gamma. Two libraries are configured, library 1 and library 2. Library 1 registers the callout "authorize" for hook alpha, "check" for hook beta and "add_option" for hook gamma. Library 2 registers "logpkt", "validate" and "putopt"

The horizontal red lines represent arguments to callouts. When the server calls hook alpha, it creates an argument list and calls the first callout for the hook, "authorize". When that callout returns, the same (but possibly modified) argument list is passed to the next callout in the chain, "logpkt". Another, separate argument list is created for hook beta and passed to the callouts "check" and "validate" in that order. A similar sequence occurs for hook gamma.

The next picture shows the scope of the context associated with a request.

DataScopeContext.png
Illustration of per-library context

The vertical blue lines represent callout context. Context is per-packet but also per-library. When the server calls "authorize", the CalloutHandle's getContext and setContext methods access a context created purely for library 1. The next callout on the hook will access context created for library 2. These contexts are passed to the callouts associated with the next hook. So when "check" is called, it gets the context data that was set by "authorize", when "validate" is called, it gets the context data set by "logpkt".

It is stressed that the context for callouts associated with different libraries is entirely separate. For example, suppose "authorize" sets the CalloutHandle's context item "foo" to 2 and "logpkt" sets an item of the same name to the string "bar". When "check" accesses the context item "foo", it gets a value of 2; when "validate" accesses an item of the same name, it gets the value "bar".

It is also stressed that all this context exists only for the life of the request being processed. When that request is complete, all the context associated with that request - for all libraries - is destroyed, and new context created for the next request.

This structure means that library authors can use per-request context without worrying about the presence of other libraries. Other libraries may be present, but will not affect the context values set by a library's callouts.

Configuring multiple libraries just requires listing the libraries as separate elements of the hooks-libraries configuration element, e.g.

"Dhcp4": {
:
"hooks-libraries": [ "/usr/lib/library1.so", "/opt/library2.so" ]
:
}

Passing Data Between Libraries

In rare cases, it is possible that one library may want to pass data to another. This can be done in a limited way by means of the CalloutHandle's setArgument and getArgument calls. For example, in the above diagram, the callout "add_option" can pass a value to "putopt" by setting a name.value pair in the hook's argument list. "putopt" would be able to read this, but would not be able to return information back to "add_option".

All argument names used by Kea will be a combination of letters (both upper- and lower-case), digits, hyphens and underscores: no other characters will be used. As argument names are simple strings, it is suggested that if such a mechanism be used, the names of the data values passed between the libraries include a special character such as the dollar symbol or percent sign. In this way there is no danger that a name will conflict with any existing or future Kea argument names.

Dynamic Callout Registration and Multiple Libraries

On a particular hook, callouts are called in the order the libraries appear in the configuration and, within a library, in the order the callouts are registered.

This order applies to dynamically-registered callouts as well. As an example, consider the diagram above where for hook "beta", callout "check" is followed by callout "validate". Suppose that when "authorize" is run, it registers a new callout ("double_check") on hook "beta". That callout will be inserted at the end of the callouts registered by library 1 and before any registered by library 2. It would therefore appear between "check" and "validate". On the other hand, if it were "logpkt" that registered the new callout, "double_check" would appear after "validate".

Running Against a Statically-Linked Kea

If Kea is built with the –enable-static-link switch (set when running the "configure" script), no shared Kea libraries are built; instead, archive libraries are created and Kea is linked to them. If you create a hooks library also linked against these archive libraries, when the library is loaded you end up with two copies of the library code, one in Kea and one in your library.

To run successfully, your library needs to perform run-time initialization of the Kea code in your library (something performed by Kea in the case of shared libraries). To do this, call the function isc::hooks::hooksStaticLinkInit() as the first statement of the load() function. (If your library does not include a load() function, you need to add one.) For example:

#include <hooks/hooks.h>
extern "C" {
int version() {
return (BIND10_HOOKS_VERSION);
}
int load() {
:
}
// Other callout functions
:
}