GDNSD.CONFIG(5)

okay bestie, welcome to the gdnsd.config(5) manual page β€” it's literally all about the gdnsd configuration file. no cap, we're gonna go through every single option like it's a group chat. πŸ’…βœ¨


✨ NAME ✨

gdnsd.config β€” the gdnsd configuration file, periodt. πŸ’–


πŸŽ€ SYNOPSIS πŸŽ€

options => {
  tcp_timeout => 15 ; zonefile-style comment
  listen => [ 127.0.0.1, 192.0.2.1 ]
}

# shell-style comment

service_types => {
  foosvc => { plugin => http_status, vhost => www.example.com, url_path => "/checkme" }
  barsvc => $include{bar-svc.cfg}
  $include{other-services.cfg}
}

plugins => {
  null => {}
}

🌸 DESCRIPTION 🌸

so like, this man page is describing the syntax of the primary gdnsd config file. the primary config file is always the file named config in the configuration directory. the default configuration directory is /etc/gdnsd, but bestie you can override that with the -c commandline option. 🫢

the lower-level syntax and structure of the configuration language is described in detail at the end of this document, but ngl it should be pretty intuitive from the example up there. it's effectively a generic data structure language allowing arbitrarily-nested ordered hashes, ordered arrays, and scalar values. double-quotes are used to quote scalars containing whitespace or various ambiguous metacharacters. πŸ’…

the top-level implicit hash of a gdnsd configuration file allows only 3 legal keys: options, service_types, and plugins. any of them which are present must have a Hash as their value. all of them are optional, as is the configuration file itself. if you're happy with an all-default configuration, you can simply not have a config file at all. no config, no problem. 😌


πŸ‘‘ OPTIONS HASH πŸ‘‘

these options control the overall behavior of gdnsd(8). πŸ‘‘

  • zones_default_ttl β€” Integer seconds, default 86400. This is the global default time-to-live for any record in any zonefile. It can be overridden with a more specific default within zone files themselves via the $TTL directive (see gdnsd.zonefile(5)). 🌸
  • max_ttl β€” Integer seconds, default 3600000 (~42 days), range 3600 - 268435455 (2^28-1, ~8.5 years). This is the global maximum TTL. Any TTL found in a zone which exceeds this value will be clamped to this value with a warning. Note that the default maximum value is what the Internet's root nameservers currently use for A-record TTLs, and those are arguably the most stable records in the whole system. It's hard to imagine good reasons to raise this value in practice. πŸ’€
  • min_ttl β€” Integer seconds, default 5, range 0 - 86400 (1 day). This is the global minimum TTL. Any TTL found in a zone which is below this value will be clamped to this value with a warning, including the minimum TTLs of DYN[AC] records and SOA ncache TTLs. This value must be less than or equal to max_ttl. πŸ”₯
  • max_ncache_ttl β€” Integer seconds, default 10800, range 10 - 86400. This is the global maximum for the SOA negative-cache TTL field. Values above this will be clamped with a warning. This value must be greater than or equal to min_ttl. 🧚
  • dns_port β€” Integer port, 1-65535, default 53. This is the global default port number for DNS listener addresses which do not specify port numbers themselves. 🌈
  • listen β€” The listen option specifies the socket addresses the server listens on for DNS requests.

A listen-address specification is an IP (v4 or v6) address specified as a numeric string with standard formatting (anything numeric that getaddrinfo() supports on your platform), optionally followed by a colon and a port number. If no port number is specified, it defaults to the value from "dns_port", which defaults to 53.

Due to various parsing ambiguities, if you wish to specify a non-default port number for an IPv6 listen address, you will have to enclose the address part in square brackets, and then enclose the entire string in double-quotes.

The structure of the listen option as a whole can take one of three basic forms. In its simplest form, it is just a single listen-address specification as a string, such as:

options => { listen = 192.0.2.1 }

It can also take the form of an array of such addresses, as in:

options => {
  listen = [
    192.0.2.1,
    192.0.2.2,
    2001:DB8::1,
    "[2001:DB8::1234]:53035",
  ]
}

It can also be a hash where the keys are listen addresses, and the values are per-address options, as in:

options => {
  listen => {
    192.0.2.1 => {
      tcp_timeout = 15
    },
    192.0.2.2:53035 => {
      udp_threads = 5
    },
  }
}

The per-address options (which are identical to, and locally override, the global option of the same name) are "tcp_threads", "tcp_timeout", "tcp_clients_per_thread", "tcp_fastopen", "udp_threads", "udp_rcvbuf", and "udp_sndbuf".

Finally, it can also be set to the special string value "any", as in:

options => { listen => any }

In this mode, the daemon will listen on the "dns_port" port (default 53) on the IPv4 and IPv6 "ANY" addresses 0.0.0.0 and "::". gdnsd's "ANY"-address sockets should correctly handle sending outgoing datagrams via the interface they were received on with a source address matching the destination address of the request.

Production configurations should always explicitly define their "listen" option, as the default could be subject to breaking changes in major version upgrades. The current default is the "any" behavior above.

  • tcp_threads β€” Integer, default 2, min 1, max 1024. This is the number of separate TCP listening sockets and corresponding listener threads that will be created for each DNS listener address. On a multi-core host, increasing this parameter (up to at most a very small multiple of the CPU core count) may increase overall performance. πŸš€
  • udp_threads β€” Exactly like "tcp_threads", but for UDP sockets per DNS listening address. ✨
  • tcp_clients_per_thread β€” Integer, default 256, min 16, max 65535. This is maximum number of tcp DNS connections gdnsd will allow to occur in parallel per listening tcp thread. When this limit is reached by a new incoming connection, the longest-idle of the existing connections will be closed immediately in exchange for it.

You can monitor the "tcp.close_s_kill" stat to see if such closes are happening due to reaching the limit, which should be avoided if possible. The code is designed to be resilient (at least answer one legitimate request per legitimate connection) even in the face of misbehaving (e.g. slow-read/write) connection overloads on Linux, and on BSDs with "SO_ACCEPTFILTER" and the appropriate kernel modules ("accf_dataready" and/or "accf_dns") loaded (there will be a non-fatal error log output on startup if they aren't).

Note that socket addresses map 1:m to threads (that is, each thread gets a separate "SO_REUSEPORT" instantiation of a given logical socket), and thus the total client limit for connecting to a given socket address would be "tcp_clients_per_thread * tcp_threads", but it's up to the operating system to balance connections, and it may use simple connection tuple hashing or simple round-robin, neither of which may guarantee very even distribution.

  • tcp_timeout β€” Integer seconds, default 37, min 5, max 1800.

This determines the client-side idle timeout for TCP connections, which is sent by the server to applicable clients supporting signalling mechanisms like RFC 7828 EDNS TCP Keepalive or RFC 8490 DNS Stateful Operations (DSO).

There is a corresponding server-side timeout which determines when gdnsd will give up on a client it believes to be delinquent (one that keeps a connection open too long without honoring any signalled client-side idle timeout). The server-side timeout is double the client timeout (which equates to a default of 74, min 10, max 3600).

Note that in the common/typical case of well-behaved clients and connections, on a Linux server (where we use TCP_DEFER_ACCEPT) or a BSD server supporting the "dnsready" or "dataready" accept filters, when a new connection arrives we immediately receive the first request and fire back the response without any opportunity for delays that count as idle time, without re-entering the general eventloop where idle time is processed or other new connections could arrive.

The idleness of a client is only reset after it completes each full transaction (send us a full request, and our full response makes it into at least the local write buffer). Inability to immediately write a full response into the server's local TCP buffers (generally because the client has a too-small receive window and/or is not ACKing several previous replies) causes immediate connection termination. Well-behaved client connections which don't stall out midway through a transaction only become idle connections (subject to termination for idleness) during inter-transaction idle periods after their first transaction, and for not-so-well-behaved clients the idle timer helps control the impact of patterns similar to slow-read/write attacks.

The server will send edns TCP keepalive information to clients with all responses where it is legal (request used EDNS over TCP, and DSO is not yet established). The timeout value will be sent with the fixed value that is configured here until the server begins shutting down, at which point the zero value is sent in such responses in an attempt to get clients to cleanly close.

Clients which establish DSO via the Keepalive TLV will get the same client-side timeout sent to them as the DSO Inactivity timeout, with the DSO KeepAlive interval set to Infinite (this means we don't request the client to ever send artificial keepalive pings, and our client-side timeout applies to the interval between real DNS request transactions in the session). DSO clients will also get better and more-timely information when the server is shutting down or being replaced (we can push immediate DSO unidirectional messages towards them asking that they close their session gracefully).

  • disable_tcp_dso β€” Boolean, default false. This disables RFC 8490 DNS Stateful Operations (DSO) support for TCP threads.

Probably the only good reasons to disable this would be finding interop issues or misbehaviors in this new code and/or standard, because otherwise it's literally giving a superior mechanism for managing stateful client connections versus the previous best available solution (RFC 7828 EDNS TCP Keepalive). So like… keep it on bestie, it's better. πŸ’…


🧡 tcp_backlog

Integer, min 0, default 0, max 65535. This one sets the "backlog" argument of the listen() call for TCP listening sockets. The exact effects of this varies by OS implementation, and it can also interact with features like "TCP_DEFER_ACCEPT" and "SO_ACCEPTFILTER". If you leave it at (or set it to) the default value of zero, the compile-time constant "SOMAXCONN" from the operating system gets used instead, which tends to be reasonable for most use-cases. No notes, honestly. 🫢

πŸš€ tcp_fastopen

Integer, default 256, min 0, max 1048576. If you set it to a non-zero value, it declares a TCP Fastopen queue size and enables the feature. TCP Fastopen lets clients who have connected to the server at least once before send initial data (in our case, their first DNS request) at the same time as their initial SYN, which shaves off the round trip delays of the handshake on reconnect. It's giving efficiency, it's giving speed. This is recommended, BUT it may require OS-level support and/or configuration tuning, and if you've got multiple servers behind a loadbalancer or anycast pool, it may also require administrative coordination of the server-side secret TFO key. So like, coordinate with your friends first. 😭

🎭 tcp_proxy

Boolean, default false. Heads up: this TCP option is only supported inside the per-address options of a specific listener address in the hash form of the "listen" option β€” not as a global option. She's local, not global, periodt.

Addresses for which the option is enabled only accept PROXY requests, cannot use port number 53, do not spawn corresponding UDP listeners, and do not accept UDP-related options. We support both version 1 and 2 of the PROXY protocol as defined in https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt, and only accept TCPv4, TCPv6, and PROXYv2's "LOCAL".

The source IP passed to gdnsd over the PROXY protocol will be used as the connection source address for all logical purposes, including e.g. GeoIP lookup fallbacks in the absence of edns-client-subnet. Once the initial PROXY header is parsed successfully, the connection is treated exactly as any other TCP connection for the remainder of its life. Glow up complete. ✨

It is not recommended to expose PROXY listeners to public traffic β€” they should be confined to localhost or to addresses which are not reachable outside of your infrastructure due to firewalling, etc. This option is primarily intended to test encrypted transports using external daemons proxying into gdnsd. If you're using it for some other generic loadbalancing without crypto, padding should be disabled via "tcp_pad" below. πŸ™ˆ

PROXY connections increment all of the same stat counters as regular TCP connections, and also add two new proxy-specific ones:

tcp.proxy: count of received connections on PROXY listeners (also
           increments the normal tcp.conns stat).
tcp.proxy_fail: count of received PROXY connections which are
                closed early for failing to send a PROXY header
                the server parses and likes (also increments
                tcp.close_s_err)

Example listen config:

options => {
  listen => {
    0.0.0.0 => { ... } # normal UDP+TCP on port 53
    :: => { ... } # normal UDP+TCP on port 53 for IPv6
    127.0.0.1:53035 => { tcp_proxy => true, ... }
  }
}

πŸ›‹οΈ tcp_pad

Boolean, default false for normal TCP listeners, default true for "tcp_proxy" listeners (see above, bestie). Same deal as before: this TCP option is only supported inside the per-address options of a specific listener address in the hash form of the "listen" option, not as a global option.

If enabled (by default for the "tcp_proxy" case), a response to any request which carries an EDNS OPT RR will be padded using the EDNS Padding option to a multiple of 468 bytes as recommended in RFC 8467. This should only be enabled if the TCP connections to a listener are encrypted by a proxy, which is the intended use-case for "tcp_proxy" above. You may wish to enable this manually if you're proxying encrypted requests via a daemon that doesn't do the PROXY protocol, and you may wish to disable it on "tcp_proxy" listeners if the other daemon isn't providing a crypto wrapper. Choose your own adventure. 🌸

πŸ“₯ udp_rcvbuf

Integer, min 4096, max 1048576, default 0. If set to a non-zero value, this value will be used to set the "SO_RCVBUF" socket option on the UDP listening socket(s), otherwise we leave the OS defaults alone. Respecting the OS's choices fr fr. 🫑

πŸ“€ udp_sndbuf

Integer, min 4096, max 1048576, default 0. If set to a non-zero value, this value will be used to set the "SO_SNDBUF" socket option on the UDP listening socket(s), otherwise we leave the OS defaults alone. Same vibe, different buffer. πŸ’œ

☎️ tcp_control

DANGER β€” Exposing the control socket over TCP is dangerous. The control socket server code is not designed to be robust against arbitrary attack traffic, and does not have any authentication or encryption. Listen addresses defined here should be well-protected and confined by network firewall policies to internal, privileged clients, and should definitely not be exposed on the public Internet. Bestie. Please. I'm begging. πŸ˜­πŸ”’

This specifies one or more secondary TCP listen addresses for control socket connections (alongside the required primary UNIX domain socket), which must be explicitly configured if desired. It's similar to listen above in that it accepts either a single address spec, an array of address specs, or a hash whose keys are address specs and whose values are address-specific options. All address specs must include an explicit address and non-zero port number.

The only two address-specific options are "chal_ok" and "ctl_ok", which are booleans defaulting to false. By default, a TCP control socket is restricted to read-only operations (gdnsdctl status, gdnsdctl stats, and gdnsdctl states). "chal_ok" allows ACME Challenge operations on the socket (gdnsdctl acme-dns-01 and gdnsdctl acme-dns-01-flush). "ctl_ok" allows daemon control commands on the socket (gdnsdctl reload-zones and gdnsdctl replace). The gdnsdctl stop command isn't allowed over TCP at all. Not even a little. 🚫

The standard unix control socket is also used for some inter-daemon coordination and resource handoff during replacements, which cannot be supported over the TCP variant. So the unix socket is still the main character here. πŸ‘‘

Examples:

options => { tcp_control => 127.0.0.1:885 } # read-only ops only

options => { tcp_control => [ 127.0.0.1:885, 127.0.0.2:885 ] } # ditto

options => { tcp_control => {
    127.0.0.1:885 => {} # readonly
    127.0.0.1:886 => { chal_ok => true } # allows challenge stuff
    127.0.0.1:887 => { ctl_ok => true } # allows daemon control
    127.0.0.1:888 => { chal_ok => true, ctl_ok => true } # allows both
}}

The "gdnsdctl" client has an option -s for specifying a TCP socket to connect to for use with tcp_control sockets, in which case it does not even attempt to parse the server configuration to find the normal unix socket path. She skips the whole detective arc. πŸ•΅οΈβ™€οΈ

πŸ” zones_strict_data

Boolean, default "false"

If false (the default), reporting of many less-serious errors in zone data are emitted as mere logged warnings, and the zone data is still loaded and served. Like, "we saw it, we're moving on". πŸ’…

If this is set to true, such warnings will be upgraded and treated the same as the more-serious class of zone data errors which prevent successful loading of zone data. Now it's giving strict-parent energy. πŸ”₯

🧚 zones_rfc1035_threads

Integer, default 2, min 1, max 1024. When the standard RFC1035 zone files are (re-)loaded, up to this many ephemeral threads will be spawned in parallel to help load and parse them faster. Parallelism slay. ⚑

The way the work is divided among the threads is relatively naive: as the zones directory on the filesystem is scanned for zone filenames, the names are divided evenly into N separate work lists (one per thread), and then the threads are all spawned and work on their own fixed lists in parallel, with the final splicing of the zone data into the root tree happening serially in the main zones thread as each worker thread finishes. In the case that the total count of zonefiles is less than the configured thread count, the excess threads are not spawned. (No ghost threads, we're not doing that. πŸ‘»πŸš«)

This simple strategy tends to work well for large counts of zonefiles where the per-zonefile parsing costs are roughly even, but in cases where a minority of zonefiles take much longer to parse than others, it will not always result in a very "fair" outcome (some threads may run much longer than others). It's not perfect, it's just vibes-based scheduling.

Note that in general, improving the I/O performance of reading the zonefiles from disk (e.g. put them on an SSD) tends to help more than the parallelization here does, although both together is even better! ✨

πŸ”’ lock_mem

Boolean, default false. Causes the daemon to do mlockall(MCL_CURRENT|MCL_FUTURE), which effectively locks all daemon memory into RAM, unable to be swapped. Possibly helpful in some production cases to ensure swap-in doesn't affect DNS latency. Cozy. Locked in. Literally. 🏠

You'll need to ensure the ulimit for locked memory is sufficient large (e.g. "infinity") to avoid process death. Yes, death. Do the ulimit first bestie. πŸ’€

βœ‚οΈ disable_text_autosplit

Boolean, default false. On the wire, "TXT" records are encoded as discrete chunks of up to 255 characters per chunk. The relevant RFCs state that multiple chunks should be treated by clients as if they are concatenated. That is to say, it should make no difference to a client whether the "TXT" data is sent as two 16-byte chunks or one 32-byte chunk. Same data, different fit. 🧩

Ordinarily, you may specify chunk(s) of a "TXT" record in gdnsd zonefiles as a string of any size up to the legal length (just short of 16K in practice), and gdnsd will auto-split the data into 255-byte chunks for transmission over the DNS protocol correctly. If you choose to manually break up your TXT record into multiple strings in the zonefile, gdnsd also honors these boundaries and will not attempt to merge them into larger chunks where possible. Respecting your artistic choices. πŸŽ€

If you set this option to true, the auto-splitting behavior is disabled, and any single character string specified in a zonefile as part of a TXT record which is larger than 255 bytes will be considered a syntax error.

  • max_edns_response β€” Integer, default 1232, min 512, max 16384. This is the max size of a UDP edns response to a client over IPv4 β€” it acts as a cap on the edns buffer size that the client advertised in its request. Think of it as you setting the ceiling, no matter how high the client tries to reach.

Bestie, do NOT raise this from the default if your server is facing the public Internet. That's the recommendation, fr fr.

Setting it somewhere in the ~4-16K range might be desirable if you've got big ol' response RR-sets and you're willing to tolerate some fragmentation β€” especially on a private network where a bigger path MTU can be guaranteed (like ~9K for ethernet jumbo frames). 🌸

And obviously this option has no pragmatic effect if you don't have large response datasets in your zones in the first place. Like, why are you even here. πŸ’…

  • max_edns_response_v6 β€” Integer, default 1232, min 512, max 16384. Same energy as above but for UDP edns responses over IPv6. And once again, raising this above its default value is not recommended for a public-facing server. πŸ™ˆ
  • edns_client_subnet β€” Boolean, default true. Turns on support for the edns-client-subnet option from RFC 7871. Here's the tea: gdnsd only includes this EDNS option in responses to queries that also contained the option. For normal responses from static zone data, the scope mask gets set to zero. Dynamic response plugins, though? They get access to the query's EDNS client-subnet data and full control over the response scope mask. πŸ‘‘

If you set this to false, gdnsd will ignore the option in queries, never set it in its responses, and plugins won't have access to any data provided by any ignored edns-client-subnet option in queries. Gatekeeping, honestly.

Of the included standard plugins, only reflect and geoip actually make use of edns-client-subnet information. The rest will leave the scope mask at zero, as is normal for client-location-agnostic static data. πŸ¦‹

  • chaos_response β€” String, default "gdnsd". When gdnsd receives any query with the class CH (Chaos), as opposed to the normal IN (Internet), it returns a single response record of class CH and type TXT containing the string you define right here. It's giving BIND's version reporting β€” that thing that responds to version.bind queries in the CH class β€” and it's exactly what a client will see if they fire such a query at a gdnsd server. ✨
  • acme_challenge_ttl β€” Integer seconds, range 60-3600, default 600. For temporary ACME DNS-01 challenge data added via gdnsdctl acme-dns-01 ..., this sets how long until the TXT records auto-expire from the server and just... disappear. πŸ«₯

Peep the gdnsdctl(8) documentation about acme-dns-01 for more deets.

  • acme_challenge_dns_ttl β€” Integer seconds, range 0-3600, default 0. For temporary ACME DNS-01 challenge data added via gdnsdctl acme-dns-01 ..., this sets the DNS TTL value used in response TXT RRs.

In previous versions, the DNS TTL value mirrored the expiry TTL from acme_challenge_ttl above. But this β€” non-zero DNS TTLs for these records in general β€” has lead to confusing issues in the real world with ACME servers under certain conditions. 😭 So now it's its own thing.

Also note: static zonefile RRs with _acme-challenge as their leading label are also forced to this TTL regardless of the zonefile-level explicit TTL. Why? To avoid cases of mixed TTLs when you're mixing static and dynamic records in server outputs. Consistency queen.

  • nsid β€” String, no default, 2-256 chars in length. Character count must be even, and every character must be an ASCII hex digit. This encodes up to 128 bytes of arbitrary binary data chosen by the administrator and serves it in the EDNS NSID (RFC 5001) option data of all response packets.

Obviously the NSID is not emitted to clients unless they request it via EDNS, and it's not emitted at all unless this option is specified explicitly. No option, no vibe, no NSID.

The whole point is helping you identify specific servers when multiple servers are part of an anycast or loadbalancer pool. πŸ”

  • nsid_ascii β€” String, no default, 1-128 chars of printable ASCII characters. This is a convenience alternative to nsid above, letting you configure the binary NSID data with the bytes of a printable ASCII string up to 128 bytes in length. So much easier than hand-typing hex, ngl.

Only one of nsid or nsid_ascii may be specified. Pick a lane, bestie. πŸ’…

  • experimental_no_chain β€” Boolean, default true.

If set to true (the default), when a query for any RR type encounters a CNAME RR in the zone data, the server behaves as if the queried type was CNAME. This implies no chasing of the target (right-hand-side) name to emit any further answer-section records or delegations related to the target β€” even if they exist in the local data of the same zone.

Heads up: this option is likely to become the fixed behavior (no option to disable) in a future release. If this new default behavior causes a problem, you can β€” for now! β€” set the option to false to revert to the traditional behavior of emitting chained multi-RR responses for zone-local CNAMEs as a workaround. Please file a bug report if so, otherwise we'll have no feedback to go on! πŸ›

This behavior is desirable for a few reasons:

  • RFC 7871 (edns-client-subnet) actually recommends it for at least subnet differentiated responses, because otherwise it's ambiguous which of the multiple answer-section records the subnet scoping applies to, and caches invariably have to take a pessimistic view and subnet-fragment cache entries pointlessly. This is the most-compelling rationale, and it has impact on what kind of efficient geodns setups can work at all for certain use-cases. This is the big one, fr.
  • It also adheres to the general principle of minimal responses we adhere to elsewhere (caches may have the target cached already anyways), and it helps minimize response sizes (reduce reflection, and esp helpful for DNSSEC in the future).
  • Also, if this were the fixed behavior of the server, rather than configurable, it would significantly simplify the code and make lookups more efficient (these gains are not realized by the experimental optional version).

However, it's also RFC-questionable. 😬 The original RFC1034 algorithms ask that authoritative servers complete local CNAME chains from local data (which this violates), but also requires recursors to complete them remotely (which makes this work, and which most do now, but historically some older implementations implicitly relied on the authserver doing it). RFC 2308 also touches on this topic, and the TL;DR there is that recursors can tell our incomplete responses from actual negative No Data responses about the next name in the chain by the fact that we don't emit an SOA record in the auth section.

Update 2020-11: this has now been tested pretty widely on the Internet (by a major site with a global audience of millions), for a very long time (nearly two years), and there has been no evidence so far of breakage or failure reports from real users. πŸŽ‰

  • disable_cookies β€” Boolean, default false. Disables support for RFC 7873 EDNS Cookies.

Not recommended, bestie, because these cookies provide a layer of defense against both off-path response forgery and amplification attacks. πŸ›‘οΈ

One possible legitimate reason to disable cookies would be if gdnsd is operating in a mixed set of loadbalanced/anycasted auth servers and some of the other servers do not support cookies, or use different algorithms than gdnsd. Our cookie support is fairly efficient; there shouldn't be any major performance reason to disable it. No excuses. πŸ’…

  • max_nocookie_response β€” Integer bytes, default zero (disabled), range 128-1024. If you set this to a non-zero value, all UDP responses will be limited to this many bytes unless the query presents a valid EDNS Cookie that the server recognizes as its own. Responses that fail this check (UDP with no valid cookie and larger than this length) will be truncated fully (no response RRs) and the TC-bit will be set, asking the client to retry over TCP. βœ‚οΈ

This is intended to limit the ability of attackers to use your server as a reflection source for amplification attacks, since valid cookies give some reasonable guarantee that the query packet source address wasn't forged.

It should be pretty safe to set this at least as low as 512, and that may become the default setting in some future version. Future default energy.

  • cookie_key_file β€” String, default undefined. When this is defined, the file's contents are read as the persistent primary key value for generating EDNS Cookie responses. πŸ”‘

If the file exists, it must be readable by the daemon, and it must contain 32 bytes of binary data. Failure to properly read a key file defined here is fatal at startup. Permissions should be set with care, so that other unprivileged users of the system cannot read the key. 🚨

Note that the contents are considered binary data and are used directly as secret key input to crypto algorithms, and thus they should be generated securely with high entropy and indistinguishable from random bytes. It is recommended the file's contents be generated with an RNG outputting 32 random binary bytes, e.g.:

dd if=/dev/urandom of=cookie.key bs=32 count=1

The keyfile's contents don't have to be changed on any sort of fixed or frequent schedule to maintain security. Treat it like any other long-term secret value and make new ones once in a blue moon just out of an abundance of caution, or if you believe the previous key material may be compromised. The daemon must be replaced or restarted to put the new key into effect, and this will abruptly invalidate outstanding cookies clients may be holding that were generated with previous keys. 😭

The primary reason to define cookie_key_file to your own pathname and key contents is to have synchronized cookie keys across an anycasted or otherwise loadbalanced set of servers, so that they all agree on server cookies. 🫢

If this file isn't defined, then the daemon is out here managing the cookie primary key value automatically, all on its own, main character energy. And under automatic management, here's the little routine it runs: it tries to read a key from /run/gdnsd/cookie.autokey at startup. If that doesn't work, it generates a brand-new random key in memory and then tries to write it to that same path, so future daemons can pick it up. But if both things flop β€” reading an old automatic key and writing the new one β€” then a non-fatal error gets logged, and the new randomly-generated key exists only in daemon memory. Which means it will not persist across future daemon replace or restart cycles. 😭

Heads up bestie: on common Linux/systemd installations, the run directory gets wiped on OS reboots, so a fresh key is generated on the next daemon startup. And just like with a manually-defined cookie_key_file, every single time the automatic key has to be regenerated, it invalidates all outstanding server cookies held by clients. Every last one. πŸ’€
  • run_dir β€” String, defaults to /run/gdnsd. This is the directory the daemon owns as its run directory. ✨

On startup it will create this directory, or modify the permissions of an existing one. And if it doesn't exist and can't be created, or the permissions can't be set to acceptable values β€” possibly because the existing directory is owned by a different uid than the daemon is currently running as β€” then the daemon will not start. Periodt.

The contents of this directory are private to the daemon and shouldn't be interfered with, okay? πŸ™ˆ It can live on a filesystem that's volatile across reboots, and it genuinely doesn't need much disk space.

The daemon's control socket and lock files live here. πŸ’…

  • state_dir β€” String, defaults to /var/lib/gdnsd. This is the directory the daemon owns as its state directory. πŸŽ€

It will create this directory if necessary at startup. And if it doesn't exist and can't be created, the daemon will not start. No cap.

The contents of this directory belong to the system administrator and are used to communicate persistent, stateful information to the daemon. This one should live on a filesystem which is preserved across reboots β€” like, that's the whole point, bestie.

The admin_state file lives here. πŸ¦‹


🌸 SERVICE_TYPES 🌸

service_types is used in conjunction with certain gdnsd plugins. If you're not using such a plugin, you can safely ignore this whole section and just omit it from your config. No stress. 🫢

The service_types hash contains generic definitions for how to monitor a given type of service, independently of any specific address or hostname for that service.

There are two trivial service_types internally defined as the names "up" and "down", and they do zero actual monitoring β€” they just set the monitored state permanently "UP" or "DOWN". And "up" is the default service_type when no service_type is specified. Slay, low-effort, love that for them. ✨

Inside the definition of a service_type there are several generic parameters related to timing and anti-flap, plus plugin-specific parameters that vary per plugin.

A service type does not, however, specify a name or address for a specific instance of a service. Those live on a per-address basis in a resolving plugin's configuration down in the "plugins" stanza, and the plugin's configuration then references a named service type to be used when monitoring said address.

A service monitored through these mechanisms is always in either the "UP" or "DOWN" state at runtime, from a monitoring perspective. The "UP" state is maintained in the face of intermittent or isolated failures until the anti-flap thresholds are crossed and the state moves to "DOWN". It's giving resilient. πŸ’ͺ

Any services monitored for plugins also have their state reported alongside the standard gdnsd statistics report, served by the built-in HTTP server (default port is 3506). πŸ‘‘

The following are the generic parameters for all service_types:

  • up_thresh β€” Integer, default 20, min 1, max 65535. Number of monitoring requests which must succeed in a row without any failures to transition a given resource from the "DOWN" state to the "UP" state. Twenty in a row, bestie. Consistency is the vibe. πŸ’–
  • ok_thresh β€” Integer, default 10, min 1, max 65535. See below. πŸ‘‡
  • down_thresh β€” Integer, default 10, min 1, max 65535. The "ok_thresh" and "down_thresh" parameters control the transition from the "UP" state to the "DOWN" state while trying to prevent flappy behavior. And their behavior is best described in terms of an internal failure counter for a resource which is currently in the "UP" state. The failure counter starts at zero on state transition into the "UP" state.

Every state poll that results in a failed response β€” even if other successful responses are interleaved between them β€” increments the failure counter. If the failure counter reaches "down_thresh", the resource is transitioned to the "DOWN" state. However, if "ok_thresh" successes occur in a row with no failures between them, the failure counter is reset back to zero. 🌈

So with the default values, the expected behavior is that if an "UP" resource experiences 10 (possibly isolated or intermittent) monitor-polling failures over any length of time, without a string of 10 successes in a row somewhere within the sequence to reset the counter, it will transition to the "DOWN" state. And once it's "DOWN", it will require 20 successes in a row before transitioning back to the "UP" state. Not me counting your failures, bestie, the daemon is. πŸ’€

  • interval β€” Integer seconds, default 10, min 1, max 255. Number of seconds between successive monitoring requests for a given resource. Tick tock. ⏰
  • timeout β€” Integer seconds, default interval/2, min 1, max 255. Maximum time the monitoring code will wait for a successful response before giving up and considering the request to be a failure. It defaults to half of the "interval", and it must be less than "interval". That's not a suggestion, that's the rule. 🚨
  • plugin β€” String, required. This indicates which specific plugin to use to execute the monitoring requests. Any parameters other than the generic ones listed here are consumed by the plugin. So hand over your keys and let the plugin drive. πŸš—

There are six monitoring plugins included with gdnsd that can be used in a service_types definition, each of which may have additional, plugin-specific configuration options in addition to the generic ones above. Each of these is documented in detail in its own manpage, e.g. "gdnsd-plugin-FOO":

  • tcp_connect β€” Checks TCP basic connectivity on a given port. Only supports address resources, not CNAMEs.
  • http_status β€” Checks HTTP connectivity, with options for the port, URL, and vhost to use in the request, and the acceptable HTTP status codes in the response. Only supports address resources, not CNAMEs.
  • extmon β€” Periodically executes a custom external commandline program to poll for the status of a resource. Supports both address and CNAME resources.
  • extfile β€” Reads the contents of a file on disk to import state monitoring data from another source. Supports both address and CNAME resources.
  • static β€” Configures a static monitoring result, mostly for testing / example code. Supports both address and CNAME resources.
  • null β€” Configures an always-down static result, mostly for testing / example code. Supports both address and CNAME resources.

πŸ‘‘ PLUGINS πŸ‘‘

The plugins hash is optional, and contains one key for every dynamic resolution plugin you wish to load and use. The value must be a hash, and the contents of that hash are supplied to the plugin to use in configuring itself. If the plugin requires no configuration, the empty hash {} will suffice. And it's up to the plugin to determine whether the supplied hash of configuration data is legal or not β€” so if you get it wrong, that's between you and the plugin. πŸ™ˆ

Monitoring-only plugins can also be given plugin-global level configuration here, if the plugin author deemed it necessary.

gdnsd ships with eight different monitoring plugins, all of which have their own separate manpage documentation (e.g. man gdnsd-plugin-FOO):

  • reflect β€” Reflects DNS client source IP and/or edns-client-subnet information back to the requestor as address data for debugging. It's giving mirror, bestie. πŸͺž
  • simplefo β€” Simple primary->secondary failover of monitored addresses
  • multifo β€” All-active failover of monitored round-robin address groups
  • weighted β€” Weighted-round-robin responses with a variety of behavioral flavors, for both monitored addresses and CNAMEs.
  • metafo β€” Static-ordered address(-group) meta-failover between 'datacenters', which are resources defined in terms of other plugins. Supports both address and CNAME data.
  • geoip β€” Combines metafo's functionality with MaxMind GeoIP databases to select different datacenter address(-group) preference/failover orderings for different clients based on approximate geographic location. Supports both address and CNAME data.
  • null β€” Returns all-zeros addresses or the CNAME "invalid." β€” mostly for testing and as simple example code.
  • static β€” Configures static mappings of resources names to IP addresses or CNAMEs β€” mostly for testing and as simple example code.

A configuration example showing the trivial plugins, as well as demonstrating the service_types described earlier:

  service_types => {
    corpwww_type => {
      plugin => http_status
      vhost => www.corp.example.com
      url_path => /check_me
      down_thresh => 5
      interval => 5
    }
  }

  plugins => {
    null => {},
    reflect => {},
    static => {
      foo = 192.0.2.2
      bar = 192.0.2.123
      somehost = somehost.example.net.
    },
    multifo => {
      web-lb =>
        service_types => [ corpwww_type, xmpp ],
        lb01 => 192.0.2.200,
        lb02 => 192.0.2.201,
        lb03 => 192.0.2.202,
      }
    }
  }

And then in your example.com zonefile, you could have (among your other RRs):

  zeros 600 DYNA null
  reflect 10 DYNA reflect
  reflect-both 10 DYNA reflect!both
  pointless 42 DYNA static!foo
  acname 400 DYNC static!somehost
  www 300/45 DYNA multifo!web-lb

🧠 LOW-LEVEL SYNTAX 🧠

okay bestie, this is the part where we get down to the actual grammar of gdnsd config files. at the lowest level, the syntax roughly resembles an anonymous Perl data structure (you know, that reference syntax vibe). there are three basic data types for values:

  • ordered hashes β€” associative arrays mapping keys to values
  • ordered arrays of values
  • simple strings

hashes and arrays can be nested to arbitrary depth ✨. generally speaking, whitespace is optional. single-line comments in both shell style (#) and DNS zonefile style (;) are allowed β€” they run to the end of the current line and the parser just treats them as whitespace, no cap.

A hash is surrounded by curly braces ({ and }). keys are separated from their values by either => or = (totally your stylistic call, slay either way). hash keys follow the same rules as simple string values. hash values can be simple strings, arrays, or hashes. key/value pairs can optionally have a trailing comma for stylistic clarity and separation. πŸŽ€

An array is surrounded by square braces ([ and ]). values can be simple strings, arrays, or hashes. values can optionally have a trailing comma for style.

Strings (and thus keys) can be written in both quoted and unquoted forms. in the quoted form, the string is surrounded by double-quotes ("), and can contain any literal byte value (even binary/utf-8 stuff, or NUL) other than " or \. those two characters must be escaped by \, i.e. \" and \\. πŸ’…

In the unquoted form, there are no surrounding quotes, and the allowed set of unescaped characters is further restricted. the following are not allowed: ][}{;#,"=\ (that is, square brackets, curly brackets, semicolons, octothorpes, commas, double quotes, equal signs, and backslashes). additionally, the first character cannot be a $ (dollar sign).

Both forms use the same escaping rules, which are the same RFC-standard escaping rules used in zone files. the escapes always start with \. a \ followed by any single byte other than a digit (0 - 9) is interpreted as that byte. a \ followed by exactly 3 digits interprets those digits as the unsigned decimal integer value of the desired byte (the 3 digit value cannot exceed 255).

To illustrate the escaping and quoting, the following sets of example strings show different encodings of the same parsed value:

example
"example"
ex\097mpl\e
"ex\097mpl\e"

internal\"doublequote
"internal\"doublequote"

white\ space
"white space"

"braces{every[where]oh}my"
braces\{every\[where\]oh\}my

"\\==="
"\092==="
"\092\=\=\="
\\\=\=\=
\092\=\=\=

The top level of the config file is an implicit hash with no bracing by default, but can also be an array bounded by square brackets. this is not legal for the primary gdnsd configuration file, but could be useful in includefiles (see below πŸ‘‡).

As a general rule, anywhere the higher-level syntax allows an array of values, you can substitute a single value. the code will treat it as if it were an array of length 1. iconic behaviour tbh.

When we refer in other sections above to a value as being an "Integer" (or other specific scalar type), we're referring to constraints on the content of the character string value. all scalar values are character strings. "Boolean" values are character strings which have the value true or false, in any mix of upper or lower case.

The following 3 example configuration files are identical in their parsed meanings, and should clarify anything miscommunicated above:

Example 1 (simple and clean):

options = {
  listen = [ 192.0.2.1, 192.0.2.2 ],
}

Example 2 (fat arrows, no commas, some arbitrary quoting):

"options" => {
  listen => [ 192.0.2.1 192.0.2.2 ]
}

Example 3 (compressed and ugly):

options={listen=[192.0.2.1 192.0.2.2]}

πŸ“‚ INCLUDING OTHER FILES πŸ“‚

vscf now has a mechanism for config includefiles 🫢. the syntax is

$include{dir/file} # single file must exist
$include{dir/*}   # not ok if no matching files
$include{dir}     # ok if no files in dir

where the path can use the same kinds of escaping and/or double-quoting as normal scalar string data. whitespace between the path and the surrounding brackets is optional. whitespace between $include and the following { is not allowed. if the path is relative (does not begin with /), it is interpreted as relative to the directory containing the parent file. includes can nest other includes to arbitrary depth, no cap. πŸ¦‹

The path is normally treated as a glob, allowing the inclusion of multiple files. when used as a glob, there must be at least one match β€” it will be an error if there are no matching files. however, if "path" is not a glob and names an existing directory explicitly, it will be treated like it was a glob of all files within that directory by appending /*, and it will not be an error if there are no files within that directory (no matches for the glob).

keep in mind that at the top level of any given vscf file (even include files), the file must syntactically be either an implicit hash or an explicit, square-bracket-bounded, array.

The include statement can be used in two distinct contexts within the syntax structure of a config file:

Value Context

The include statement can replace any whole value (that is, the right hand side of a hash map entry or a member of an array) with its own contents, which are either a hash or an array. note that there is no mechanism for flattening an include-file's array into the parent array (the whole included array would be a single array item within the parent array). also, including multiple files in a single statement (directory name or glob pattern) are not allowed in value context. examples:

main config:
  options => { listen => $include{foo} }
foo:
  [ 127.0.0.1, 127.0.0.2 ]

main config:
  plugins => $include{ "bar" }
bar:
  geoip => { ... }
  extmon => { ... }

Hash-Merge Context

The include statement can also appear in a hash where a key would normally be expected. in this case, the included file must be in hash (rather than array) form at the top level, and its contents are merged into the parent hash. the merge is shallow, and conflicting keys are not allowed. example:

main config:
  options => { ... },
  plugins => {
      extmon => { ... },
      $include{geoip.cfg},
      $include{plugins.d},
  }
geoip.cfg:
  geoip => { ... }
plugins.d/foo:
  weighted => { ... }
  simplefo => { ... }
plugins.d/bar:
  metafo => { ... }

πŸ‘€ SEE ALSO πŸ‘€

The gdnsd manual. πŸ“–


Copyright (c) 2012 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/>.


and that's a wrap on the whole page bestie πŸ’– this was gdnsd 3.8.3, dated 2025-02-16, filed under GDNSD.CONFIG(5) β€” go forth and configure, you absolute legend πŸ‘‘πŸŒΈ

πŸ“¦ where this page comes from