gdnsd-plugin-api(3) How to write gdnsd plugin code

SYNOPSIS


Mandatory preamble macro+header your source must include at the top:
#define GDNSD_PLUGIN_NAME foo
#include <gdnsd/plugin.h>
Callback hooks you may implement (all are optional, and executed in this order):
(Letters in brackets denote callbacks applicable to: R for Resolver plugin role
and/or M for Monitor plugin role; a plugin may implement one or both).
-- startup/config stuff:
# only 'checkconf', 'start', 'restart', 'condrestart' invoke plugin callbacks at all
[RM] void plugin_foo_load_config(vscf_data_t* pc, const unsigned num_threads)
[ M] void plugin_foo_add_svctype(const char* name, vscf_data_t* svc_cfg, const unsigned interval, const unsigned timeout)
[ M] void plugin_foo_add_mon_addr(const char* desc, const char* svc_name, const char* cname, const dmn_anysin_t* addr, const unsigned idx);
[ M] void plugin_foo_add_mon_cname(const char* desc, const char* svc_name, const char* cname, const unsigned idx);
# only 'start', 'restart', and 'condrestart' continue past this point
[ M] void plugin_foo_init_monitors(struct ev_loop* mon_loop)
[ M] void plugin_foo_start_monitors(struct ev_loop* mon_loop)
[R ] void plugin_foo_pre_run()
[R ] void plugin_foo_iothread_init(unsigned threadnum)
-- runtime stuff (called from main or zonefile thread)
-- (you won't get parallel calls to this, and in general it should be a readonly
-- operation anyways)
[R ] int plugin_foo_map_res(const char* resname, const uint8_t* origin)
-- runtime stuff (called from iothread context, anytime after iothread_init())
[R ] gdnsd_sttl_t plugin_foo_resolve(unsigned resnum, const uint8_t* origin, const client_info_t* cinfo, dyn_result_t* result)
-- cleanup stuff:
[RM] void plugin_foo_exit(void)

WARNING

Please note that in general, gdnsd's plugin API is poorly documented and unstable. It often goes through fairly large and abrupt changes during development cycles, although it tends to be stable for a given stable release series. Write code against it at your own peril (or at least, let me know so I can give you some warning on upcoming changes and/or solicit your feedback!).

OVERVIEW

This file documents versions 15-16 of the gdnsd plugin API.

gdnsd's plugin API offers the ability to write plugins that can do either (or both) of two roles:

1) Dynamically generate virtual "A", "AAAA", and/or "CNAME" records according to whatever logic the plugin author wishes. The plugin can make use of gdnsd's monitoring services for being failover-aware, and the actual zonefile records that trigger these lookups are "DYNA" (for address-only data) and "DYNC" (for which the plugin can return "CNAME" or address results).

2) Provide custom protocols and implementations for the back-end of the monitoring code for use by any plugin. In this case you mostly just implement the protocol check code against a standard libev event loop and use a helper function to report the results of each status check, and the core takes care of the rest.

All callbacks can be implemented by all plugins; it is possible to create a combined plugin that performs both roles. There is no clear distinction between plugin ``types'' internally.

USER-LEVEL CONFIGURATION FOR RESOLVER PLUGINS

If you haven't read the documentation for the overall configuration file (gdnsd.config) and the zonefiles (gdnsd.zonefile), you might want to read those before continuing.

From a user's perspective, there are two parts to configuring plugins. The first is configuring the plugin via the gdnsd config file. The config file has an optional "plugins" hash. The keys of this hash are the names of plugins to load, and the values (which must be hashes) are the configuration data for the plugin itself. e.g., to load two plugins named "foo" and "bar", the plugins hash might look like this:

  plugins => {
    foo => {
       opts => {
          something = "quux\000<-an_embedded_null!",
          somethingelse = { Z => z },
       },
       xyz = [x, y, z]
    }
    bar => { x => y }
  }

Note that a short-form plugin name (e.g. "foo") maps to a shared library named plugin_foo.so. Plugins will be loaded from the directory /usr/lib/x86_64-linux-gnu/gdnsd by default, but this path can be overridden in the "options" section of the gdnsd configuration.

The basic syntactic structure of your plugin's config hash follows the same rules as the gdnsd config as a whole. This is the "vscf" syntax, which allows the user to specify nested data in the form of hashes, arrays, and simple values. It's entirely up to the plugin author how the contents of the hash should be interpreted, and to document the plugin's config hash for users.

The second part of the configuration is inserting "DYNA" and/or "DYNC" resource records into zonefiles. "DYNA" RRs use a plugin to dynamically generate "A" and/or "AAAA" RRs, while "DYNC" RRs use a plugin to dynamically generate either "A"/"AAAA" RRs or "CNAME" RRs.

  www      300 DYNA foo!prod_web
  www.test 300 DYNA foo!test_web
  web      300 DYNC bar!test_web_cname

The initial parts (the left-hand domainname, TTL, and RR-type) follow the usual zonefile norms, other than the fact that "DYNA" is not a real resource record type in the DNS protocol. The rdata section (e.g. "foo!prod_web") contains two parts separated by an "!": A plugin name, and a resource name.

The meaning of the resource name is entirely up to the plugin. Typically it will reference a configuration key from the plugin's configuration hash as a mapping to a specific set of parameters for the plugin, but other uses of this field are possible.

Plugins may implement just address results, just CNAME results, or both.

USER-LEVEL CONFIGURATION FOR MONITORING

DYNA/DYNC plugin code can optionally take advantage of monitoring services, e.g. to not return ``dead'' addresses from a pool. Monitoring is configured as a set of "service_types", each representing a protocol, protocol-specific parameters, and some generic parameters related to timing and anti-flap. e.g.:

    service_types = {
        prod_web = {
            plugin = http_status
            # plugin-specific parameters
            vhost = www.example.com
            url_path = /checkme
            ok_codes = [ 200, 201 ]
            # generic parameters
            up_thresh = 24
            down_thresh = 16
            ok_thresh = 8
            interval = 8
            timeout = 4
        }
    }

A service type is meant to be re-used to monitor the same service at several different addresses or CNAMEs.

One of the service type parameters is "plugin", naming a custom monitoring plugin to load. If this plugin was not listed directly in the "plugins" hash to give it global-level configuration, it will be loaded with no configuration at all ("_load_config(NULL)").

PLUGIN SOURCE ORGANIZATION

There must be one primary plugin source file which implements the callback hooks, and this file must include the following before any other code:

    #define GDNSD_PLUGIN_NAME foo
    #include <gdnsd/plugin.h>

If you wish to split your implementation over multiple files, you can access the relevant API interfaces via the other "gdnsd/*.h" headers directly. However all of the actual callback hooks must be implemented in the primary source file, and your other source files should not include "gdnsd/plugin.h".

RUNTIME CALLBACK FLOW

To understand how plugins operate and how to write plugins, it is necessary to understand the overall flow of gdnsd's execution, and where in that flow various callbacks are made into the code of the loaded plugins. If you haven't yet read the main gdnsd daemon documentation at this point, now would be a good time, as it covers some basic info about how gdnsd acts as its own initscript. All callbacks have the name of the plugin in the function name, and we will use the example name "foo" for documentation purposes. A brief summary of all of the API interfaces and semantics follows in a later section, but it would be good to read through this lengthy prose explanation at least once.

CONFIGURATION

When gdnsd is started via actions such as "start", "restart" or "condrestart", or when configuration is checked via "checkconf", at least some of the plugin callbacks will be executed.

As soon as the configuration file as a whole has been validated and loaded, gdnsd goes about setting various internal parameters from this data. When it encounters the "plugins" hash, it will load and configure the named plugins. Immediately after loading each plugin, it will execute the "plugin_foo_load_config()" callback, providing the plugin code with its vscf configuration hash. At this time the plugin should walk (and validate) the provided configuration data and set up its own internal parameters based on this data. Any expensive configuration steps should be avoided in the load_config callback. Your goal in load_config is to validate your configuration data and store it somewhere, nothing more.

There are 3 special API calls that are only valid during the execution of "plugin_foo_load_config()" and only by resolver plugins, which are used by the plugin to feed some configuration-based data back to the core code. These are "gdnsd_mon_addr()" and "gdnsd_mon_cname()" (which are used by resolver plugins to ask the monitoring system to monitor addresses and/or CNAMEs), and "gdnsd_dyn_addr_max()", which must be called to inform the core code of the maximum address counts this plugin configuration could ever return in a single response. Failure to call "gdnsd_dyn_addr_max()" results in the core assuming a maximum of 1 address per family.

Next, "service_types" are processed from the config. These may autoload additional plugins that were not specified in the "plugins" hash. They will also receive a "plugin_foo_load_config(NULL)" call if autoloaded.

For each service type that uses a given plugin, the plugin will receive a "plugin_foo_add_svctype()" callback. Use this to set up local data structures for each service type you've been assigned.

Next, all of the specific monitoring requested earlier by resolver plugins (via "gdnsd_mon_addr()" and "gdnsd_mon_cname()") is passed to the monitoring plugins by invoking their "plugin_foo_add_mon_addr()" and "plugin_foo_add_mon_cname()". This is when a monitoring plugin sets up per-address/CNAME data structures.

After all of the above, the daemon loads and parses all zonefiles, constructing the internal runtime DNS database. During the zonefile loading phase, when it encounters "DYNA" RRs in zonefiles, they will trigger the plugin callback "plugin_foo_map_res" once for every "DYNA" RR, with a "NULL" "origin" argument. The same occurs with all "DYNC" RRs, and they will get non-"NULL" "origin" arguments, which indicate the current $ORIGIN in effect for the RR. It is important to note that your plugin should treat it as an error if it gets a "_map_res" call with a "NULL" "origin" (DYNA) for a resource which is configured to be capable of returning "CNAME" results.

If your DYNC plugin supports variable origins (e.g. the same resource name can be re-used in multiple zonefiles, and prepends some standard domainname fragment to origin in effect for the given RR), it is important that you validate that you can construct a legal domainname (length limits) from the given origin, resource name, and your own config at this time.

Plugins should not return different resource numbers for the same resname argument regardless of "origin" value (or lack thereof). You will break things if you do so.

If your map_resource operation fails (e.g. unknown resource name, or illegal origin-based "CNAME" construction, or a NULL origin argument (DYNA) for a resource that could return "CNAME" data), log the error and return -1. Do not fail fatally, as these calls happen at runtime during dynamic zonefile reloads.

In the case of the action "checkconf", execution stops here. Only the "start" and "restart" actions continue on to become full-fledged daemon instances.

The first is "plugin_foo_init_monitors()". You will be passed the event loop, and you are expected to set up events that will do a single monitoring check on all monitored resources and then clear themselves and not repeat. When all plugins have done their init_monitors(), the loop will be run, and it is expected to terminate after a few seconds when all monitoring states have been initialized with real-world data.

The next is "plugin_foo_start_monitors()". Again you are passed the same libev loop, and you add all of your monitored resource callbacks, but this time it's permanent: they're expected to repeat their monitoring checks endlessly the next time the loop is invoked.

When your libev monitoring callbacks have determined a success or failure for a monitored resource, they're expected to call the helper function "gdnsd_mon_state_updater()" from gdnsd/mon.h to send the state info upstream for anti-flap calculations and re-destribution to plugins which are monitoring the given resource.

"plugin_foo_pre_run" is executed next, giving a final chance to run any single-threaded setup code before threads are spawned and we enter runtime operations.

After pre_run, gdnsd will spawn the runtime DNS I/O threads. For each such thread, the callback "plugin_foo_iothread_init" will be called from within each I/O thread with the global thread number as the only argument (0 through num_threads-1, where num_threads was provided to you back at "plugin_foo_load_config()" time). This would be the ideal time to xmalloc() writable per-thread data structures from within the threads themselves, so that a thread-aware malloc can avoid false sharing.

RUNTIME

At this point, gdnsd is ready to begin serving DNS queries. After all I/O threads have finished initialization (and thus moved on to already serving requests), the primary thread will do its own thing for managing daemon lifecycle and signals and such.

During runtime the only direct callbacks your plugin will receive from I/O thread contexts are "plugin_foo_resolve" and "plugin_foo_map_res".

As a general style rule, the runtime resolver callback is not allowed to block or fail. It is expected to respond immediately with valid response data. It is your job as the plugin author to ensure this is the case. That means pre-allocating memory, pre-loading data, and/or pre-calculating anything expensive during earlier callbacks. Worst case, you can return meaningless data, e.g. 0.0.0.0 for "DYNA" or some hostname like "plugin.is.broken." for "DYNC", but ideally all possible error conditions have been checked out beforehand.

"_resolve" is supplied with a resource number, a result structure your code can use to supply address information to the client, a "client_info_t" structure giving network information about the querying client, and an "origin" argument.

The resource number and origin will match with earlier "map_res" calls your plugin received.

The "client_info_t" structure contains the querying DNS cache's address as well as optional edns-client-subnet address+mask information. If the mask is zero, there was no (useful) edns-client-subnet information, and the plugin must fall back to using the cache's address. When edns-client-subnet information is present, the edns-client-subnet output ``scope'' mask must be set in the result structure (to zero if the information went unused, or to a specific scope as defined in the edns-client-subnet draft (could be shorter or longer than the client's specified mask)).

There is no distinction between A and AAAA requests (for that matter, your plugin could be invoked to provide Additional-section addresses for other requested types like MX or SRV). You must answer with all applicable IPv4 and IPv6 addresses on every call. Generally speaking, gdnsd treats A and AAAA much like a single RR-set. Both are always included in the additional section when appropriate. In response to a direct query for A or AAAA, the daemon returns the queried address RR type in the answer section and the other in the additional section.

Results are added to the opaque "dyn_result_t*" via the various "gdnsd_result_*()" calls.

The "gdnsd_sttl_t" return value of the resolve callback is used for your plugin to indicate the up/down state and TTL of the response placed in the "dyn_result_t", which is used to carry these values upwards through nested meta-plugins (e.g. multifo -> metafo -> geoip).

The "map_res" callback may also be called at any time during normal runtime as a result of zonefiles being dynamically reloaded. These should be readonly operations so there shouldn't be any locking concerns. It's important that these calls never fail fatally. Simply log an error and return -1.

At the time of daemon exit, "plugin_foo_exit()" may be called in developer builds as a hook to e.g. unwind complex runtime memory allocation routines for valgrind verification. It's never called in regular production builds.

THREADING

gdnsd uses POSIX threads. Only the runtime resolve callbacks "plugin_foo_map_res" and "plugin_foo_resolve" need to to concern themselves with thread safety. They can and will be called from multiple POSIX threads simultaneously for runtime requests.

The simplest (but least-performant) way to ensure thread-safety would be to wrap the contents of this function in a pthread mutex. However, for most imaginable cases, it should be trivial to structure your data and code such that this function can be both lock-free and thread-safe.

CORE API DETAILS

These are the functions exported by the core gdnsd code, which are available for your plugin to call at runtime. They're implemented in a library named "libgdnsd", which the gdnsd daemon has already loaded before loading your plugin. You don't need to (and shouldn't) explicitly link against libgdnsd. The interfaces are defined in a set of header files grouped by functionality. Note that in your primary plugin source file which includes gdnsd/plugin.h, all of these header files have already been included for you indirectly.

For now, the documentation of these interfaces exists solely in the header files themselves. I'm still trying to sort out how to document them correctly, probably doxygen.

gdnsd/compiler.h
gdnsd/plugapi.h
gdnsd/vscf.h
gdnsd/net.h
gdnsd/misc.h
gdnsd/log.h
gdnsd/mon.h
gdnsd/dname.h

GENERAL PLUGIN CODING CONVENTIONS, ETC

logging and errors
All syslog/stderr -type output should be handled via the thread-safe "log_*()" and "logf_*()" calls provided by gdnsd. Do not attempt to use stderr (or stdout/stdin) or syslog directly. To throw a fatal error and abort daemon execution, use "log_fatal()", which does not return.
debugging
Build your plugin with "-DNDEBUG" unless you're actually debugging development code, and make liberal use of "dmn_assert()" and "log_debug()" where applicable.
prototypes and headers
You do not declare function prototypes for the callback functions (plugin_foo_*). The prototypes are declared for you when you include the gdnsd/plugin.h header. You need merely define the functions themselves.
API versioning
There is an internal API version number documented at the top of this document and set in "gdnsd/plugapi.h". This number is only incremented when incompatible changes are made to the plugin API interface or semantics which require recompiling plugins and/or updating their code. When gdnsd is compiled this version number is hardcoded into the daemon binary. When plugins are compiled the API version they were built against is also hardcoded into the plugin object automatically. When gdnsd loads a plugin object, it checks for an exact match of plugin API version. If the number does not match, a fatal error will be thrown telling the user the plugin needs to be rebuilt against the gdnsd version in use.

The current API version number is available to your code as the macro "GDNSD_PLUGIN_API_VERSION". If necessary, you can test this value via "#if" macro logic to use alternate code for different API versions (or simply to error out if the API version is too old for your plugin code).

map_res consistency
The "_map_res()" callback, if implemented, must return a consistent, singular resource number for a given resource name, regardless of any "origin" argument or the lack thereof.
ignoring origin for address-only data
If a plugin only handles addresses (for this resource, or in the general case), it should not fail on "_map_res()" or "_resolve()" just because an origin is defined, indicating a "DYNC" RR. It should instead simply ignore any origin argument and act as it always did.
map_res DYNA validity checks
If a resource name passed to "_map_res()" is configured to be capable of returning "CNAME" data and the "origin" argument is "NULL" (indicating a "DYNA" RR), the plugin must fail by returning -1. One of the implications of this rule is that for any plugin which is capable of returning "CNAME" data at all, "_map_res()" must be implemented. Another implication of this (combined with the consistency rule) is that it's no longer legal to structure plugin resources such that they have unrelated sets of address and "CNAME" data stored under the same resource name, as the weighted plugin originally did before its matching set of changes.

RECENT API CHANGES

Version 17

This corresponds with the release of 2.2.0

Changes versus version 16:

"gdnsd_dname_isparentof()" removed (can be trivially replaced using "gdnsd_dname_isinzone()" if necessary).

The PRNG interfaces have changed completely. The old interface returned a "gdnsd_rstate_t*" from the call "gdnsd_rand_init()", which could then be passed to either of "gdnsd_rand_get32()" or "gdnsd_rand_get64()" to get unsigned 32-bit or 64-bit random numbers, respectively. The replacement interface has split the 32-bit and 64-bit random number generators into separate interfaces and state structures.

For a 32-bit PRNG, call "gdnsd_rand32_init()" which returns a "gdnsd_rstate32_t*", which can then be passed to "gdnsd_rand32_get()" to obtain unsigned 32-bit random numbers.

For a 64-bit PRNG, call "gdnsd_rand64_init()" which returns a "gdnsd_rstate64_t*", which can then be passed to "gdnsd_rand64_get()" to obtain unsigned 64-bit random numbers.

Version 15/16

This corresponds with the release of 2.0.0 and 2.1.0

The changes below are versus Version 12 (final gdnsd 1.x API version). Versions 13 and 14 only existed in development releases and were moving targets. The changes from Version 12 were rather sweeping. This tries to cover the largest notable changes in the key callbacks, but likely doesn't note them all. When in doubt, look at the source of the core plugins distributed with the main source for guidance.

The data structures "dynaddr_result_t" and "dyncname_result_t" were merged and replaced with a single structure "dyn_result_t", which is an opaque data structure modified by the various "gdnsd_result_*()" functions for adding or clearing address and/or CNAME results.

The "_map_res_dyna()" and "_map_res_dync()" callbacks were merged and renamed to just "_map_res()". The new call has an origin argument like the old "_map_res_dync()", which will be NULL when called for "DYNA" RRs, and the "result" argument's type was changed from "dynaddr_result_t*" to "dyn_result_t*".

The "_resolve_dynaddr()" and "_resolve_dyncname()" callbacks were merged and renamed to just "_resolve()". The new call has an origin argument like the old "_resolve_dyncame()", which will be NULL when called for "DYNA" RRs, and the "result" argument's type was changed from "dynaddr_result_t*" to "dyn_result_t*". The new call also lacks the "threadnum" argument, as any plugin which needs this information can work around it via the "_iothread_init()" callback and/or thread-local storage.

"gdnsd_dynaddr_add_result_anysin()" was renamed to "gdnsd_dyn_add_result_anysin()", and the "result" argument's type was changed from "dynaddr_result_t*" to "dyn_result_t*".

"_load_config()" no longer has a "mon_list_t*" return value; instead monitored resources are indicated to the core via the "gdnsd_mon_addr()" and "gdnsd_mon_cname()" functions during the execution of "_load_config()".

Resolver plugins must now call "gdnsd_dyn_addr_max()" during "_load_config()" to inform the core of address limits.

"gdnsd_add_monitor" was replaced by "gdnsd_add_mon_addr()" and "gdnsd_add_mon_cname()".

The callbacks "_post_daemonize()", "_pre_privdrop()", "_post_privdrop()", and "_full_config()" were removed. With the current structure, code that logically fit in these can be placed elsewhere (e.g. "_start_monitors()", "_pre_run()", or "_load_config()" as appropriate).

The type "vscf_data_t*" in callback arguments used to be "const", and now it is not. Similar changes occurred in many places in the vscf API in general. Just remove const from your plugin's local vscf pointers and recompile.

Version 16 was bumped just to require a recompile (some formerly-exported funcs became inlines, some const changes in signatures, etc), but is mostly the same as 15 otherwise.

COPYRIGHT AND LICENSE

Copyright (c) 2014 Brandon L Black <[email protected]>

This file is part of gdnsd.

gdnsd is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

gdnsd is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with gdnsd. If not, see <http://www.gnu.org/licenses/>.