NAME¶
gdnsd-plugin-api - 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(struct ev_loop* loop)
[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 enter the libev loop mentioned above and
remain under its control until daemon exit time. During this time the only
direct callback your plugin will receive from I/O thread contexts is
"plugin_foo_resolve". If you registered any libev watchers during
pre_run, you will also receive relevant callbacks there in the monitoring
thread's execution context.
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).
When a signal is sent to stop the daemon, the primary thread's libev loop will
return and no further watcher callbacks (set up via
pre_run() or
start_monitors()) will be invoked. The main daemon will
syslog()
some final stats output and invoke "exit()" to terminate the
process. This in turn unwinds the stack of "atexit()" handlers.
First will be a handler which cancels and joins all of the outstanding I/O
threads, then comes another which invokes each plugin's
"plugin_foo_exit()" from the main thread's context. You can log any
stats output you wish here, and/or destruct any resources you wish (no worry
about races from io thread access, they're all dead now).
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.
THREADING¶
gdnsd uses POSIX threads. Only the runtime resolve callback
"plugin_foo_resolve" needs to to concern itself with thread safety.
It 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 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 occured 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.
SEE ALSO¶
The source for the included addr/cname-resolution plugins "null",
"reflect", "static", "simplefo",
"multifo", "weighted", "metafo", and
"geoip". The source for the included monitoring plugins
"http_status", "tcp_connect", and "extmon".
gdnsd(8),
gdnsd.config(5),
gdnsd.zonefile(5)
The gdnsd manual.
COPYRIGHT AND LICENSE¶
Copyright (c) 2014 Brandon L Black <blblack@gmail.com>
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/>.