Writing custom handlers¶
Writing a custom handler is, together with writing custom options, the most common way of customising the DHCPKit server. Handlers process the incoming message and adapt the outgoing message. There are many things a handler could do. Some of the common use cases are:
- assigning addresses/prefixes to incoming
IANAOption
,IATAOption
andIAPDOption
requests (see e.g.CSVStaticAssignmentHandler
) - providing
RecursiveNameServersOption
to clients (seeRecursiveNameServersOptionHandler
) - limiting the maximum values for T1/T2 so that clients come back often enough for renewal of their addresses (see e.g.
IANATimingLimitsHandler
)
Basic handler structure¶
All handlers must be subclasses of Handler
or RelayHandler
. Each handler must be
registered as a server extension so that the server code is aware of their existence.
A handler usually implements its functionality by overriding the handle()
method (or
handle_relay()
in the case of RelayHandler
). This method gets a
TransactionBundle
as its only parameter bundle
. The bundle contains all the information available about
a request and the response. Handlers providing information (e.g. DNS information) commonly look at whether the
client included an OptionRequestOption
in its request
and based on that
information decide to add an extra option to the response
.
Because there are several very common patterns here are some base classes you can use:
SimpleOptionHandler
which adds a static option instance to responsesOverwritingOptionHandler
which overwrites all options of the same class with adds a static option instanceCopyOptionHandler
which copies options from a certain class from the request to the responseCopyRelayOptionHandler
which copies options from a certain class from each incomingRelayForwardMessage
to the correspondingRelayReplyMessage
.
Loading handlers from the configuration file¶
There are two parts to creating new handlers that can be used in the configuration file. The first part is the XML definition of what the configuration section looks like. The second part is a factory function or object that will create the handler from the configuration.
Defining the configuration section¶
If you want your handler to be loadable from the configuration file you need to provide a ZConfig
component.xml
schema file that determines what your configuration section will look like. A configuration section
definition can look like this:
<component xmlns="https://raw.githubusercontent.com/zopefoundation/ZConfig/master/doc/schema.dtd"
prefix="dhcpkit.ipv6.server.extensions.dns.config">
<sectiontype name="recursive-name-servers"
extends="option_handler_factory_base"
implements="handler_factory"
datatype=".RecursiveNameServersOptionHandlerFactory">
<description><![CDATA[
This sections adds recursive name servers to the response sent to the
client. If there are multiple sections of this type then they will be
combined into one set of recursive name servers which is sent to the
client.
]]></description>
<example><![CDATA[
<recursive-name-servers>
address 2001:4860:4860::8888
address 2001:4860:4860::8844
</recursive-name-servers>
]]></example>
<multikey name="address" attribute="addresses" required="yes"
datatype="ipaddress.IPv6Address">
<description>
The IPv6 address of a recursive name server.
</description>
<example>
2001:db8:1::53
</example>
</multikey>
</sectiontype>
<sectiontype name="domain-search-list"
extends="option_handler_factory_base"
implements="handler_factory"
datatype=".DomainSearchListOptionHandlerFactory">
<description><![CDATA[
This sections adds domain names to the domain search list sent to the
client. If there are multiple sections of this type then they will be
combined into one set of domain names which is sent to the client.
]]></description>
<example><![CDATA[
<domain-search-list>
domain-name example.com
domain-name example.net
domain-name example.org
</domain-search-list>
]]></example>
<multikey name="domain-name" attribute="domain_names" required="yes"
datatype="dhcpkit.common.server.config_datatypes.domain_name">
<description>
The domain name to add to the search list.
</description>
<example>
example.com
</example>
</multikey>
</sectiontype>
</component>
This component describes two section types: recursive-name-servers
and domain-search-list
. They both have
implements="handler_factory"
which makes them usable as a handler. The datatypes of the sections are relative to
prefix="dhcpkit.ipv6.server.extensions.dns.config"
because they start with a .
.
The datatypes of <key>
and <multikey>
elements can be one of the ZConfig predefined types or anything that can
be called like a function which accepts the string value of what the user put into the configuration file as its single
parameter. Its return value is stored as the value. This behaviour also allows you to provide a class as the datatype.
Its constructor will be called with a single string argument. In the example above you can see this for the
<multikey name="address" ...
where the datatype is the IPv6Address
class from ipaddress
.
The <description>
and <example>
tags are used when generating documentation. The whole configuration
section of this manual is created based on such tags!
Writing the handler factory¶
After parsing a section and converting its values using the specified datatypes, the datatype of the sectiontype will
be called with a ZConfig.SectionValue
object containing all the values as its only parameter. The return value
of that datatype must be callable as a function, which acts as a factory for the actual handler.
Note
The reason that a factory is used is for privilege separation. The configuration file is read as the user that
started the server process, usually root
, while the factory is called with the privileges of the user and
group that the server is configured to run as. This makes sure that e.g. all files created by a handler have the
right ownership.
The easiest way to write a handler factory is to create a subclass of HandlerFactory
and create the
Handler
in the implementation of the create()
method. Because
HandlerFactory
is a subclass of ConfigSection
you can use its functionality to assist with
processing configuration sections. If some of the values in the configuration are optional and the default value has to
be determined at runtime you can modify section
in clean_config_section()
.
If the configuration values need extra validation then do so in validate_config_section()
.
For convenience you can access the configuration values both as self.section.xyz and as self.xyz.
If you want your section to have a “name” like in:
<static-csv data/assignments.csv>
prefix-preferred-lifetime 3d
prefix-valid-lifetime 30d
</static-csv>
You can set the name_datatype
to the function or class
that should be used to parse the name.
This is a complete example that uses both the name and other section values:
class CSVStaticAssignmentHandlerFactory(HandlerFactory):
"""
Factory for a handler that reads assignments from a CSV file
"""
name_datatype = staticmethod(existing_file)
def create(self) -> CSVStaticAssignmentHandler:
"""
Create a handler of this class based on the configuration in the config section.
:return: A handler object
"""
# Get the lifetimes
address_preferred_lifetime = self.address_preferred_lifetime
address_valid_lifetime = self.address_valid_lifetime
prefix_preferred_lifetime = self.prefix_preferred_lifetime
prefix_valid_lifetime = self.prefix_valid_lifetime
return CSVStaticAssignmentHandler(
self.name,
address_preferred_lifetime, address_valid_lifetime,
prefix_preferred_lifetime, prefix_valid_lifetime
)
Handler initialisation¶
Handlers are initialised in two steps. The first step is when the factory creates the handler object. This happens in
the main server process just before the worker processes are spawned. Those worker processes get a copy of the handlers
when the worker is being initialised. This is done by pickling
the MessageHandler
and all
the filters and handlers it contains. The advantage is that workers don’t need to initialise everything themselves
(especially if that initialisation can take a long time, like when parsing a CSV file) but it also means that things
that cannot be pickled can therefore not be initialised when creating the handler. Therefore handlers have a separate
worker_init()
method that is called inside each worker. Initialisation that need to happen in each worker
process (for example opening database connections) can be done there.
Registering new handlers¶
New handlers must be registered so that the server knows which sections are available when parsing the server configuration. This is done by defining entry points in the setup script:
setup(
name='dhcpkit_demo_extension',
...
entry_points={
'dhcpkit.ipv6.server.extensions': [
'handler-name = dhcpkit_demo_extension.package',
],
},
)
If the package contains a file called component.xml
then that file is used as an extension to the configuration
file syntax.
More advanced message handling¶
If necessary a handler can do pre()
and post()
processing. Pre processing can be useful
in cases where an incoming request has to be checked to see if it should be handled at all or whether processing should
be aborted. Post processing can be used for cleaning up, checking that all required options are included in the
response, committing leases to persistent storage, etc.
The post processing stage is especially important to handlers that assign resources. In the handle()
method the handler puts its assignments in the response. That doesn’t mean that that response is actually sent to the
client. Another handler might change the response or abort the processing later.
Handlers that have to store state should do that during post processing after verifying the response. If rapid
commit is active the response might even have changed from an AdvertiseMessage
to a ReplyMessage
.
Handlers that store data based on whether a resource was only advertised or whether it was actually assigned
must look at the response being sent to determine that.
Handling rapid commit¶
Usually rapid commit is handled by its own built-in handler. If a handler does not want a rapid commit
to happen it can set the allow_rapid_commit
attribute of the transaction bundle to False.
The built-in handler will take that into account when deciding whether it performs a rapid commit or not.
Rules for handlers that assign resources¶
Options meant for assigning addresses and prefixes like IANAOption
, IATAOption
and
IAPDOption
are a bit more complex to handle. The way handlers are designed in dhcpkit is that each such
option can be handled by one handler. A handler that assigns addresses should use the
bundle.get_unhandled_options
method to find those options in the
request that haven’t been handled yet:
After handling an option the handler must mark that option as handled by calling
bundle.mark_handled
with the handled option as parameter. This will let
handlers that are executed later know which options still need to be handled.
When handling ConfirmMessage
, ReleaseMessage
and DeclineMessage
the handler should
behave as follows:
- It should mark as handled the options that it is responsible for
- If the confirm/release/decline is successful it should not modify the response
- If the confirm/release/decline is not successful it should put the appropriate options and/or status code in the response
- If a previous handler has already put a negative status code in the response then that status code should be left intact
The built-in message handler
will automatically apply handlers that check for any unhandled
options and set the status code if it hasn’t been set by any other handler.
Aborting message handling¶
There are cases where a handler decides that the current request should not be handled by this server at all.
One example is when a handler determines that the ServerIdOption
in the request refers to a difference
DUID
than that of the server. In those cases the handler can throw a CannotRespondError
exception.
This will stop all processing and prevent a response from being sent to the client.
A handler should not abort in the post processing phase. When post processing starts all handlers should be able to assume that the response is finished and that they can rely on the response being sent.
Example of a Handler¶
This is the implementation of RecursiveNameServersOptionHandler
. As you can see most of the code is for
processing the configuration data so that this handler can be added through the configuration file as described in
the Recursive-name-servers manual page.
class RecursiveNameServersOptionHandler(SimpleOptionHandler):
"""
Handler for putting RecursiveNameServersOption in responses
"""
def __init__(self, dns_servers: Iterable[IPv6Address], always_send: bool = False):
option = RecursiveNameServersOption(dns_servers=dns_servers)
option.validate()
super().__init__(option, always_send=always_send)
def __str__(self):
return "{} with {}".format(self.__class__.__name__, ', '.join(map(str, self.option.dns_servers)))
def combine(self, existing_options: Iterable[RecursiveNameServersOption]) -> RecursiveNameServersOption:
"""
Combine multiple options into one.
:param existing_options: The existing options to include name servers from
:return: The combined option
"""
addresses = []
# Add from existing options first
for option in existing_options:
for address in option.dns_servers:
if address not in addresses:
addresses.append(address)
# Then add our own
for address in self.option.dns_servers:
if address not in addresses:
addresses.append(address)
# And return a new option with the combined addresses
return RecursiveNameServersOption(dns_servers=addresses)
Example of a RelayHandler¶
This is the implementation of InterfaceIdOptionHandler
which copies InterfaceIdOption
from incoming
relay messages to outgoing relay messages. The implementation is very simple:
class InterfaceIdOptionHandler(CopyRelayOptionHandler):
"""
The handler for InterfaceIdOptions in relay messages
"""
def __init__(self):
super().__init__(InterfaceIdOption)