Skip site navigation (1)Skip section navigation (2)

FreeBSD Manual Pages

  
 
  

home | help
docs::gdnsd-plugin-api(3)	      gdnsd	       docs::gdnsd-plugin-api(3)

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()
	 [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 de-
     velopment 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 accord-
     ing to whatever logic the plugin author wishes.  The plugin can make use of
     gdnsd's monitoring services for being failover-aware, and the actual zone-
     file 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 imple-
     ment 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 configura-
     tion 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/lo-
     cal/lib/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 al-
     lows 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" re-
     source records into zonefiles.  "DYNA" RRs use a plugin to dynamically gen-
     erate "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 re-
     source 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-spe-
     cific 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 sev-
     eral 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 con-
     figuration 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 ac-
     cess the relevant API interfaces via the other "gdnsd/*.h" headers di-
     rectly.  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 func-
     tion name, and we will use the example name "foo" for documentation pur-
     poses.  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 "con-
     drestart", 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 plu-
     gins.  Immediately after loading each plugin, it will execute the plu-
     gin_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 config-
     uration 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 re-
     solver 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 re-
     turn 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 struc-
     tures 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 plu-
     gin_foo_add_mon_cname().  This is when a monitoring plugin sets up per-ad-
     dress/CNAME data structures.

     After all of the above, the daemon loads and parses all zonefiles, con-
     structing 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 ille-
     gal 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 zone-
     file reloads.

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

     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 monitor-
     ing check on all monitored resources and then clear themselves and not re-
     peat.  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 monitor-
     ing 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 moni-
     toring 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 plu-
     gin_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 "plu-
     gin.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 plu-
     gin 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_re-
     sult_*()" 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 read-
     only 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 "plu-
     gin_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 avail-
     able 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 cor-
     rectly, 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 er-
	 ror and abort daemon execution, use log_fatal(), which does not return.

     debugging
	 Build your plugin with "-DNDEBUG" unless you're actually debugging de-
	 velopment 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 (plu-
	 gin_foo_*).  The prototypes are declared for you when you include the
	 gdnsd/plugin.h header.  You need merely define the functions them-
	 selves.

     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 se-
	 mantics 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 ver-
	 sion 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, sin-
	 gular 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 gen-
	 eral 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 impli-
	 cations of this rule is that for any plugin which is capable of return-
	 ing "CNAME" data at all, _map_res() must be implemented.  Another im-
	 plication of this (combined with the consistency rule) is that it's no
	 longer legal to structure plugin resources such that they have unre-
	 lated 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 un-
     signed 32-bit or 64-bit random numbers, respectively.  The replacement in-
     terface has split the 32-bit and 64-bit random number generators into sepa-
     rate 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 ob-
     tain 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 ob-
     tain 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 dis-
     tributed 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_re-
     sult_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 _io-
     thread_init() callback and/or thread-local storage.

     gdnsd_dynaddr_add_result_anysin() was renamed to gdnsd_dyn_add_re-
     sult_anysin(), and the "result" argument's type was changed from "dy-
     naddr_result_t*" to "dyn_result_t*".

     _load_config() no longer has a "mon_list_t*" return value; instead moni-
     tored 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 logi-
     cally 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 gen-
     eral.  Just remove const from your plugin's local vscf pointers and recom-
     pile.

     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", "re-
     flect", "static", "simplefo", "multifo", "weighted", "metafo", and "geoip".
     The source for the included monitoring plugins "http_status", "tcp_con-
     nect", "extmon", and "extfile".

     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 de-
     tails.

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

gdnsd 2.4.3			   2026-08-01	       docs::gdnsd-plugin-api(3)

Want to link to this manual page? Use this URL:
<https://man.freebsd.org/cgi/man.cgi?query=gdnsd-plugin-api&sektion=3&manpath=FreeBSD+Ports+15.1.quarterly>

home | help