Inplace callbacks
=================

This example shows how to register and use inplace callback functions. These
functions are going to be called just before unbound replies back to a client.
They can perform certain actions without interrupting unbound's execution flow
(e.g. add/remove EDNS options, manipulate the reply).

Two different scenarios will be shown:

- If answering from cache and the client used EDNS option code ``65002`` we
  will answer with the same code but with data ``0xdeadbeef``;
- When answering with a SERVFAIL we also add an empty EDNS option code
  ``65003``.


Key parts
~~~~~~~~~

This example relies on the following functionalities:


Registering inplace callback functions
--------------------------------------

There are four types of inplace callback functions:

- `inplace callback reply functions`_
- `inplace callback reply_cache functions`_
- `inplace callback reply_local functions`_
- `inplace callback reply_servfail functions`_


Inplace callback reply functions
................................

Called when answering with a *resolved* query.

The callback function's prototype is the following:

.. code-block:: python

    def inplace_reply_callback(qinfo, qstate, rep, rcode, edns, opt_list_out,
                               region, **kwargs):
        """
        Function that will be registered as an inplace callback function.
        It will be called when answering with a resolved query.

        :param qinfo: query_info struct;
        :param qstate: module qstate. It contains the available opt_lists; It
                       SHOULD NOT be altered;
        :param rep: reply_info struct;
        :param rcode: return code for the query;
        :param edns: edns_data to be sent to the client side. It SHOULD NOT be
                     altered;
        :param opt_list_out: the list with the EDNS options that will be sent as a
                             reply. It can be populated with EDNS options;
        :param region: region to allocate temporary data. Needs to be used when we
                       want to append a new option to opt_list_out.
        :param **kwargs: Dictionary that may contain parameters added in a future
                         release. Current parameters:
            ``repinfo``: Reply information for a communication point (comm_reply).

        :return: True on success, False on failure.

        """

.. note:: The function's name is irrelevant.

We can register such function as:

.. code-block:: python

    if not register_inplace_cb_reply(inplace_reply_callback, env, id):
        log_info("python: Could not register inplace callback function.")


Inplace callback reply_cache functions
......................................

Called when answering *from cache*.

The callback function's prototype is the following:

.. code-block:: python

    def inplace_cache_callback(qinfo, qstate, rep, rcode, edns, opt_list_out,
                               region, **kwargs):
        """
        Function that will be registered as an inplace callback function.
        It will be called when answering from the cache.

        :param qinfo: query_info struct;
        :param qstate: module qstate. None;
        :param rep: reply_info struct;
        :param rcode: return code for the query;
        :param edns: edns_data sent from the client side. The list with the EDNS
                     options is accessible through edns.opt_list. It SHOULD NOT be
                     altered;
        :param opt_list_out: the list with the EDNS options that will be sent as a
                             reply. It can be populated with EDNS options;
        :param region: region to allocate temporary data. Needs to be used when we
                       want to append a new option to opt_list_out.
        :param **kwargs: Dictionary that may contain parameters added in a future
                         release. Current parameters:
            ``repinfo``: Reply information for a communication point (comm_reply).

        :return: True on success, False on failure.

        For demonstration purposes we want to see if EDNS option 65002 is present
        and reply with a new value.

        """

.. note:: The function's name is irrelevant.

We can register such function as:

.. code-block:: python

    if not register_inplace_cb_reply_cache(inplace_cache_callback, env, id):
        log_info("python: Could not register inplace callback function.")


Inplace callback reply_local functions
......................................

Called when answering with *local data* or a *Chaos(CH) reply*.

The callback function's prototype is the following:

.. code-block:: python

    def inplace_local_callback(qinfo, qstate, rep, rcode, edns, opt_list_out,
                               region, **kwargs):
        """
        Function that will be registered as an inplace callback function.
        It will be called when answering from local data.

        :param qinfo: query_info struct;
        :param qstate: module qstate. None;
        :param rep: reply_info struct;
        :param rcode: return code for the query;
        :param edns: edns_data sent from the client side. The list with the
                     EDNS options is accessible through edns.opt_list. It
                     SHOULD NOT be altered;
        :param opt_list_out: the list with the EDNS options that will be sent as a
                             reply. It can be populated with EDNS options;
        :param region: region to allocate temporary data. Needs to be used when we
                       want to append a new option to opt_list_out.
        :param **kwargs: Dictionary that may contain parameters added in a future
                         release. Current parameters:
            ``repinfo``: Reply information for a communication point (comm_reply).

        :return: True on success, False on failure.

        """

.. note:: The function's name is irrelevant.

We can register such function as:

.. code-block:: python

    if not register_inplace_cb_reply_local(inplace_local_callback, env, id):
        log_info("python: Could not register inplace callback function.")


Inplace callback reply_servfail functions
.........................................

Called when answering with *SERVFAIL*.

The callback function's prototype is the following:

.. code-block:: python

    def inplace_servfail_callback(qinfo, qstate, rep, rcode, edns, opt_list_out,
                                  region, **kwargs):
        """
        Function that will be registered as an inplace callback function.
        It will be called when answering with SERVFAIL.

        :param qinfo: query_info struct;
        :param qstate: module qstate. If not None the relevant opt_lists are
                       available here;
        :param rep: reply_info struct. None;
        :param rcode: return code for the query. LDNS_RCODE_SERVFAIL;
        :param edns: edns_data to be sent to the client side. If qstate is None
                     edns.opt_list contains the EDNS options sent from the client
                     side. It SHOULD NOT be altered;
        :param opt_list_out: the list with the EDNS options that will be sent as a
                             reply. It can be populated with EDNS options;
        :param region: region to allocate temporary data. Needs to be used when we
                       want to append a new option to opt_list_out.
        :param **kwargs: Dictionary that may contain parameters added in a future
                         release. Current parameters:
            ``repinfo``: Reply information for a communication point (comm_reply).

        :return: True on success, False on failure.

        For demonstration purposes we want to reply with an empty EDNS code '65003'
        and log the IP address(es) of the client(s).

        """

.. note:: The function's name is irrelevant.

We can register such function as:

.. code-block:: python

    if not register_inplace_cb_reply_servfail(inplace_servfail_callback, env, id):
        log_info("python: Could not register inplace callback function.")


Testing
~~~~~~~

Run the Unbound server: ::

    root@localhost$ unbound -dv -c ./test-inplace_callbacks.conf

In case you use your own configuration file, don't forget to enable the Python
module::

    module-config: "validator python iterator"

and use a valid script path ::

    python-script: "./examples/inplace_callbacks.py"

On the first query for the nlnetlabs.nl A record we get no EDNS option back:

::

    root@localhost$ dig @localhost nlnetlabs.nl +ednsopt=65002

    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> @localhost nlnetlabs.nl +ednsopt=65002
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48057
    ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 3

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ;; QUESTION SECTION:
    ;nlnetlabs.nl.                  IN      A

    ;; ANSWER SECTION:
    nlnetlabs.nl.           10200   IN      A       185.49.140.10

    ;; AUTHORITY SECTION:
    nlnetlabs.nl.           10200   IN      NS      ns.nlnetlabs.nl.
    nlnetlabs.nl.           10200   IN      NS      sec2.authdns.ripe.net.
    nlnetlabs.nl.           10200   IN      NS      anyns.pch.net.
    nlnetlabs.nl.           10200   IN      NS      ns-ext1.sidn.nl.

    ;; ADDITIONAL SECTION:
    ns.nlnetlabs.nl.        10200   IN      A       185.49.140.60
    ns.nlnetlabs.nl.        10200   IN      AAAA    2a04:b900::8:0:0:60

    ;; Query time: 813 msec
    ;; SERVER: 127.0.0.1#53(127.0.0.1)
    ;; WHEN: Mon Dec 05 16:15:32 CET 2016
    ;; MSG SIZE  rcvd: 204

When we issue the same query again we get a cached response and the expected
``65002: 0xdeadbeef`` EDNS option:

::

    root@localhost$ dig @localhost nlnetlabs.nl +ednsopt=65002

    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> @localhost nlnetlabs.nl +ednsopt=65002
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26489
    ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 3

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ; OPT=65002: de ad be ef ("....")
    ;; QUESTION SECTION:
    ;nlnetlabs.nl.                  IN      A

    ;; ANSWER SECTION:
    nlnetlabs.nl.           10197   IN      A       185.49.140.10

    ;; AUTHORITY SECTION:
    nlnetlabs.nl.           10197   IN      NS      ns.nlnetlabs.nl.
    nlnetlabs.nl.           10197   IN      NS      sec2.authdns.ripe.net.
    nlnetlabs.nl.           10197   IN      NS      anyns.pch.net.
    nlnetlabs.nl.           10197   IN      NS      ns-ext1.sidn.nl.

    ;; ADDITIONAL SECTION:
    ns.nlnetlabs.nl.        10197   IN      AAAA    2a04:b900::8:0:0:60
    ns.nlnetlabs.nl.        10197   IN      A       185.49.140.60

    ;; Query time: 0 msec
    ;; SERVER: 127.0.0.1#53(127.0.0.1)
    ;; WHEN: Mon Dec 05 16:50:04 CET 2016
    ;; MSG SIZE  rcvd: 212

By issuing a query for a bogus domain unbound replies with SERVFAIL and an
empty EDNS option code ``65003``. *For this example to work unbound needs to be
validating*:

::

    root@localhost$ dig @localhost bogus.nlnetlabs.nl txt

    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> @localhost bogus.nlnetlabs.nl txt
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 19865
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ; OPT=65003
    ;; QUESTION SECTION:
    ;bogus.nlnetlabs.nl.            IN      TXT

    ;; Query time: 11 msec
    ;; SERVER: 127.0.0.1#53(127.0.0.1)
    ;; WHEN: Mon Dec 05 17:06:01 CET 2016
    ;; MSG SIZE  rcvd: 51


Complete source code
~~~~~~~~~~~~~~~~~~~~
.. literalinclude:: ../../examples/inplace_callbacks.py
    :language: python
