]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Introduce structured YAML settings for Recursor.
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Wed, 5 Jul 2023 13:48:59 +0000 (15:48 +0200)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Mon, 11 Sep 2023 13:10:07 +0000 (15:10 +0200)
Mostly written in Rust, using CXX and Serde

Code generation is used to generate both the old style config tables as
the new Rust based code. The code generation also produces the code
to covert old styel to new style and documentation.

Th main entry point for code generationo is settings/generate.py,
using the table table.py

Existing configs continue to work as before.

57 files changed:
.github/actions/spell-check/expect.txt
.github/workflows/build-and-test-all.yml
.github/workflows/codeql-analysis.yml
builder-support/debian/recursor/debian-buster/rules
builder-support/dockerfiles/Dockerfile.debbuild-prepare
builder-support/dockerfiles/Dockerfile.rpmbuild
builder-support/helpers/install_rust.sh [new file with mode: 0755]
builder-support/specs/pdns-recursor.spec
pdns/arguments.cc
pdns/arguments.hh
pdns/recursordist/.gitignore
pdns/recursordist/Makefile.am
pdns/recursordist/configure.ac
pdns/recursordist/docs/appendices/example/conversion [new file with mode: 0644]
pdns/recursordist/docs/appendices/example/fwzones.txt [new file with mode: 0644]
pdns/recursordist/docs/appendices/example/generate.sh [new file with mode: 0755]
pdns/recursordist/docs/appendices/example/recursor.conf [new file with mode: 0644]
pdns/recursordist/docs/appendices/example/recursor.d/01.conf [new file with mode: 0644]
pdns/recursordist/docs/appendices/yamlconversion.rst [new file with mode: 0644]
pdns/recursordist/docs/indexTOC.rst
pdns/recursordist/docs/lua-scripting/hooks.rst
pdns/recursordist/docs/manpages/rec_control.1.rst
pdns/recursordist/docs/settings.rst
pdns/recursordist/docs/upgrade.rst
pdns/recursordist/docs/yamlsettings.rst [new file with mode: 0644]
pdns/recursordist/rec-main.cc
pdns/recursordist/rec-main.hh
pdns/recursordist/rec_channel_rec.cc
pdns/recursordist/rec_control.cc
pdns/recursordist/reczones.cc
pdns/recursordist/settings/.gitignore [new file with mode: 0644]
pdns/recursordist/settings/Makefile.am [new file with mode: 0644]
pdns/recursordist/settings/README.md [new file with mode: 0644]
pdns/recursordist/settings/cxxsettings-private.hh [new file with mode: 0644]
pdns/recursordist/settings/cxxsettings.hh [new file with mode: 0644]
pdns/recursordist/settings/cxxsupport.cc [new file with mode: 0644]
pdns/recursordist/settings/docs-new-preamble-in.rst [new file with mode: 0644]
pdns/recursordist/settings/docs-old-preamble-in.rst [new file with mode: 0644]
pdns/recursordist/settings/generate.py [new file with mode: 0644]
pdns/recursordist/settings/rust-bridge-in.rs [new file with mode: 0644]
pdns/recursordist/settings/rust-preamble-in.rs [new file with mode: 0644]
pdns/recursordist/settings/rust/.gitignore [new file with mode: 0644]
pdns/recursordist/settings/rust/Cargo.lock [new file with mode: 0644]
pdns/recursordist/settings/rust/Cargo.toml [new file with mode: 0644]
pdns/recursordist/settings/rust/Makefile.am [new file with mode: 0644]
pdns/recursordist/settings/rust/build.rs [new file with mode: 0644]
pdns/recursordist/settings/rust/src/bridge.rs [new file with mode: 0644]
pdns/recursordist/settings/rust/src/helpers.rs [new file with mode: 0644]
pdns/recursordist/settings/table.py [new file with mode: 0644]
pdns/recursordist/syncres.hh
pdns/recursordist/test-settings.cc [new file with mode: 0644]
pdns/recursordist/ws-recursor.cc
regression-tests.api/.gitignore
regression-tests.api/runtests.py
regression-tests.recursor-dnssec/recursortests.py
regression-tests.recursor-dnssec/test_SimpleYAML.py [new file with mode: 0644]
tasks.py

index b451e038dc8e4d0db5557c5b3e9509a7844c8c7c..4df2bcf119e7384c4987c460deba753725466ad3 100644 (file)
@@ -451,6 +451,7 @@ fulltoc
 fullycapable
 Furnell
 Fusl
+fwzones
 FYhvws
 FZq
 gaba
@@ -1121,7 +1122,9 @@ rdata
 rdqueries
 rdynamic
 reconnections
+recordcache
 recursor
+recursord
 recursordist
 Recursordoc
 Recuweb
@@ -1512,6 +1515,7 @@ webbased
 webdocs
 webhandler
 webpassword
+webservice
 Webspider
 Wegener
 Weimer
@@ -1559,6 +1563,8 @@ xpf
 XRecord
 XXXXXX
 yahttp
+yamlconversion
+yamlsettings
 Yehuda
 yeswehack
 Yiu
index 7d2a75fd49e20c885fc8bedd16324ba6e421cdc4..cc239d47243c9b035b678095396eb2a543d14c4d 100644 (file)
@@ -120,6 +120,7 @@ jobs:
           path: ~/.ccache
           key: recursor-${{ matrix.sanitizers }}-ccache-${{ steps.get-stamp.outputs.stamp }}
           restore-keys: recursor-${{ matrix.sanitizers }}-ccache-
+      - run: inv ci-install-rust ${{ env.REPO_HOME }}
       - run: inv ci-autoconf
       - run: inv ci-rec-configure
       - run: inv ci-rec-make-bear
index 874ec85e773a8e55ab860ab191342dc0da8c33dd..8188bd63f29c683ae14662dc316766f5ffe33f3d 100644 (file)
@@ -104,6 +104,7 @@ jobs:
                 libwslay-dev \
                 libyaml-cpp-dev \
                 ragel \
+                rustc \
                 unixodbc-dev
 
     - name: Build auth
@@ -133,6 +134,8 @@ jobs:
         autoreconf -vfi
         ./configure --enable-unit-tests --enable-nod --enable-dnstap CFLAGS='-O0' CXXFLAGS='-O0'
         make -j8 -C ext
+        make -j8 -C settings
+        make -j8 -C settings/rust
         make htmlfiles.h
         make -j4 pdns_recursor rec_control
 
index bcfd5f65a17ed2a074ca39dabc3da1d2e354d10c..9e122d632667bf1dcb901b70ec3a1906757282c4 100755 (executable)
@@ -40,6 +40,7 @@ override_dh_auto_install:
        install -d debian/pdns-recursor/usr/share/pdns-recursor/snmp
        install -m 644 -t debian/pdns-recursor/usr/share/pdns-recursor/snmp RECURSOR-MIB.txt
        rm -f debian/pdns-recursor/etc/powerdns/recursor.conf-dist
+       rm -f debian/pdns-recursor/etc/powerdns/recursor.yml-dist
        ./pdns_recursor --no-config --config=default | sed \
                -e 's!^# config-dir=.*!config-dir=/etc/powerdns!' \
                -e 's!^# hint-file=.*!&\nhint-file=/usr/share/dns/root.hints!' \
index 7f03177fe68435bf57bd1046ff8254cad7d98bf3..bf86ab18bd36f34e07cface9ccfb8c98a654c721 100644 (file)
@@ -1,11 +1,16 @@
 FROM dist-base as package-builder
 ARG APT_URL
-RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends devscripts dpkg-dev build-essential python3-venv equivs
+RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends devscripts dpkg-dev build-essential python3-venv equivs curl
 
 RUN mkdir /dist /pdns
 WORKDIR /pdns
 
 ADD builder/helpers/ /pdns/builder/helpers/
+ADD builder-support/helpers/ /pdns/builder-support/helpers/
+
+@IF [ -n "$M_recursor$M_all" ]
+RUN /pdns/builder-support/helpers/install_rust.sh
+@ENDIF
 
 # Used for -p option to only build specific packages
 ARG BUILDER_PACKAGE_MATCH
index d98a6826c63df6b4cc3c7db6ed4011c2b148886e..48bd68eb76b4cf53379099890fcc19238d73cedc 100644 (file)
@@ -1,10 +1,10 @@
 FROM dist-base as package-builder
 RUN touch /var/lib/rpm/* && if $(grep -q 'release 7' /etc/redhat-release); then \
       yum upgrade -y && \
-      yum install -y rpm-build rpmdevtools python2 python3 "@Development Tools"; \
+      yum install -y rpm-build rpmdevtools python2 python3 curl "@Development Tools"; \
     else \
       yum upgrade -y && \
-      yum install -y rpm-build rpmdevtools python3 "@Development Tools"; \
+      yum install -y rpm-build rpmdevtools python3 curl "@Development Tools"; \
     fi
 
 RUN mkdir /dist /pdns
@@ -13,6 +13,11 @@ RUN rpmdev-setuptree
 
 # Only ADD/COPY the files you really need for efficient docker caching.
 ADD builder/helpers/ /pdns/builder/helpers/
+ADD builder-support/helpers/ /pdns/builder-support/helpers/
+
+@IF [ -n "$M_recursor$M_all" ]
+RUN /pdns/builder-support/helpers/install_rust.sh
+@ENDIF
 
 # Used for -p option to only build specific spec files
 ARG BUILDER_PACKAGE_MATCH
diff --git a/builder-support/helpers/install_rust.sh b/builder-support/helpers/install_rust.sh
new file mode 100755 (executable)
index 0000000..97a766b
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+set -e
+
+ARCH=$(arch)
+
+# Default version
+RUST_VERSION=rust-1.72.0-$ARCH-unknown-linux-gnu
+
+if [ $# -ge 1 ]; then
+    RUST_VERSION=$1
+    shift
+fi
+
+SITE=https://downloads.powerdns.com/rust
+RUST_TARBALL=$RUST_VERSION.tar.gz
+
+SHA256SUM_x86_64=f2bbe23e685852104fd48d0e34ac981b0917e76c62cfcd6d0ac5283e4710c7b9
+
+NAME=SHA256SUM_$ARCH
+eval VALUE=\$$NAME
+if [ -z "$VALUE" ]; then
+    echo "$0: No SHA256 defined for $ARCH" > /dev/stderr
+    exit 1
+fi
+
+# Procedure to update the Rust tarball:
+# 1. Download tarball and signature (.asc) file from
+#    https://forge.rust-lang.org/infra/other-installation-methods.html "Standalone installers" section
+# 2. Import Rust signing key into your gpg if not already done so
+# 3. Run gpg --verify $RUST_TARBALL.asc and make sure it is OK
+# 4. Run sha256sum $RUST_TARBALL and set SHA256SUM above, don't forget to update RUST_VERSION as well
+# 5. Make $RUST_TARBALL available from https://downloads.powerdns.com/rust
+#
+cd /tmp
+echo $0: Downloading $RUST_TARBALL
+
+curl -o $RUST_TARBALL $SITE/$RUST_TARBALL
+# Line below should echo two spaces between digest and name
+echo $VALUE"  "$RUST_TARBALL | sha256sum -c -
+tar -zxf $RUST_TARBALL
+cd $RUST_VERSION
+./install.sh --prefix=/usr
+cd ..
+rm -rf $RUST_TARBALL $RUST_VERSION
index 75a2690f7ec9edd76fb3fff6661414e69643afac..8408500e98a8c4ef8ed4a6134e4331be20a5b85e 100644 (file)
@@ -125,4 +125,5 @@ systemctl daemon-reload ||:
 %dir %{_sysconfdir}/%{name}
 %dir %{_sysconfdir}/%{name}/recursor.d
 %config(noreplace) %{_sysconfdir}/%{name}/recursor.conf
+%config %{_sysconfdir}/%{name}/recursor.yml-dist
 %doc README
index a1081ddd704eaa42418f58910c187e365ac5bbe1..1831d7d07fe1df707c05d07a9b87ec51baf138c5 100644 (file)
@@ -81,11 +81,6 @@ vector<string> ArgvMap::list()
   return ret;
 }
 
-string ArgvMap::getHelp(const string& item)
-{
-  return helpmap[item];
-}
-
 string& ArgvMap::set(const string& var, const string& help)
 {
   helpmap[var] = help;
@@ -366,7 +361,8 @@ static const map<string, string> deprecateList = {
   {"xpf-rr-code", "Proxy Protocol"},
 };
 
-void ArgvMap::warnIfDeprecated(const string& var)
+// NOLINTNEXTLINE(readability-convert-member-functions-to-static): accesses d_log (compiled out in auth, hence clang-tidy message)
+void ArgvMap::warnIfDeprecated(const string& var) const
 {
   const auto msg = deprecateList.find(var);
   if (msg != deprecateList.end()) {
@@ -375,6 +371,12 @@ void ArgvMap::warnIfDeprecated(const string& var)
   }
 }
 
+string ArgvMap::isDeprecated(const string& var)
+{
+  const auto msg = deprecateList.find(var);
+  return msg != deprecateList.end() ? msg->second : "";
+}
+
 void ArgvMap::parseOne(const string& arg, const string& parseOnly, bool lax)
 {
   string var;
@@ -475,7 +477,7 @@ void ArgvMap::preParse(int& argc, char** argv, const string& arg)
   }
 }
 
-bool ArgvMap::parseFile(const char* fname, const string& arg, bool lax)
+bool ArgvMap::parseFile(const string& fname, const string& arg, bool lax)
 {
   string line;
   string pline;
@@ -523,19 +525,19 @@ bool ArgvMap::parseFile(const char* fname, const string& arg, bool lax)
   return true;
 }
 
-bool ArgvMap::preParseFile(const char* fname, const string& arg, const string& theDefault)
+bool ArgvMap::preParseFile(const string& fname, const string& arg, const string& theDefault)
 {
   d_params[arg] = theDefault;
 
   return parseFile(fname, arg, false);
 }
 
-bool ArgvMap::file(const char* fname, bool lax)
+bool ArgvMap::file(const string& fname, bool lax)
 {
   return file(fname, lax, false);
 }
 
-bool ArgvMap::file(const char* fname, bool lax, bool included)
+bool ArgvMap::file(const string& fname, bool lax, bool included)
 {
   if (!parmIsset("include-dir")) { // inject include-dir
     set("include-dir", "Directory to include configuration files from");
@@ -550,7 +552,7 @@ bool ArgvMap::file(const char* fname, bool lax, bool included)
   // handle include here (avoid re-include)
   if (!included && !d_params["include-dir"].empty()) {
     std::vector<std::string> extraConfigs;
-    gatherIncludes(extraConfigs);
+    gatherIncludes(d_params["include-dir"], ".conf", extraConfigs);
     for (const std::string& filename : extraConfigs) {
       if (!file(filename.c_str(), lax, true)) {
         SLOG(g_log << Logger::Error << filename << " could not be parsed" << std::endl,
@@ -563,30 +565,31 @@ bool ArgvMap::file(const char* fname, bool lax, bool included)
   return true;
 }
 
-void ArgvMap::gatherIncludes(std::vector<std::string>& extraConfigs)
+// NOLINTNEXTLINE(readability-convert-member-functions-to-static): accesses d_log (compiled out in auth, hence clang-tidy message)
+void ArgvMap::gatherIncludes(const std::string& directory, const std::string& suffix, std::vector<std::string>& extraConfigs)
 {
-  extraConfigs.clear();
-  if (d_params["include-dir"].empty()) {
+  if (directory.empty()) {
     return; // nothing to do
   }
 
-  DIR* dir = nullptr;
-  if ((dir = opendir(d_params["include-dir"].c_str())) == nullptr) {
+  auto dir = std::unique_ptr<DIR, decltype(&closedir)>(opendir(directory.c_str()), closedir);
+  if (dir == nullptr) {
     int err = errno;
-    string msg = d_params["include-dir"] + " is not accessible: " + stringerror(err);
+    string msg = directory + " is not accessible: " + stringerror(err);
     SLOG(g_log << Logger::Error << msg << std::endl,
-         d_log->error(Logr::Error, err, "Directory is not accessible", "name", Logging::Loggable(d_params["include-dir"])));
+         d_log->error(Logr::Error, err, "Directory is not accessible", "name", Logging::Loggable(directory)));
     throw ArgException(msg);
   }
 
+  std::vector<std::string> vec;
   struct dirent* ent = nullptr;
-  while ((ent = readdir(dir)) != nullptr) { // NOLINT(concurrency-mt-unsafe): see Linux man page
+  while ((ent = readdir(dir.get())) != nullptr) { // NOLINT(concurrency-mt-unsafe): see Linux man page
     if (ent->d_name[0] == '.') {
       continue; // skip any dots
     }
-    if (boost::ends_with(ent->d_name, ".conf")) {
+    if (boost::ends_with(ent->d_name, suffix)) {
       // build name
-      string name = d_params["include-dir"] + "/" + ent->d_name; // NOLINT: Posix API
+      string name = directory + "/" + ent->d_name; // NOLINT: Posix API
       // ensure it's readable file
       struct stat statInfo
       {
@@ -595,12 +598,11 @@ void ArgvMap::gatherIncludes(std::vector<std::string>& extraConfigs)
         string msg = name + " is not a regular file";
         SLOG(g_log << Logger::Error << msg << std::endl,
              d_log->info(Logr::Error, "Unable to open non-regular file", "name", Logging::Loggable(name)));
-        closedir(dir);
         throw ArgException(msg);
       }
-      extraConfigs.push_back(name);
+      vec.emplace_back(name);
     }
   }
-  std::sort(extraConfigs.begin(), extraConfigs.end(), CIStringComparePOSIX());
-  closedir(dir);
+  std::sort(vec.begin(), vec.end(), CIStringComparePOSIX());
+  extraConfigs.insert(extraConfigs.end(), vec.begin(), vec.end());
 }
index fa5bfcf35547aaff7ebe09b443ec0728456e85b3..cbc834be74b508b30c9f004f138360af7b4c0367 100644 (file)
@@ -81,15 +81,15 @@ public:
     parse(argc, argv, true);
   }
   void preParse(int& argc, char** argv, const string& arg); //!< use this to preparse a single var
-  bool preParseFile(const char* fname, const string& arg, const string& theDefault = ""); //!< use this to preparse a single var in configuration
+  bool preParseFile(const string& fname, const string& arg, const string& theDefault = ""); //!< use this to preparse a single var in configuration
 
-  bool file(const char* fname, bool lax = false); //!< Parses a file with parameters
-  bool file(const char* fname, bool lax, bool included);
-  bool laxFile(const char* fname)
+  bool file(const string& fname, bool lax = false); //!< Parses a file with parameters
+  bool file(const string& fname, bool lax, bool included);
+  bool laxFile(const string& fname)
   {
     return file(fname, true);
   }
-  bool parseFile(const char* fname, const string& arg, bool lax); //<! parse one line
+  bool parseFile(const string& fname, const string& arg, bool lax); //<! parse one line
   bool parmIsset(const string& var); //!< Checks if a parameter is set to *a* value
   bool mustDo(const string& var); //!< if a switch is given, if we must do something (--help)
   int asNum(const string& arg, int def = 0); //!< return a variable value as a number or the default if the variable is empty
@@ -109,15 +109,25 @@ public:
   void setDefaults();
 
   vector<string> list();
-  string getHelp(const string& item);
-
+  [[nodiscard]] string getHelp(const string& item) const
+  {
+    auto iter = helpmap.find(item);
+    return iter == helpmap.end() ? "" : iter->second;
+  }
+  [[nodiscard]] string getDefault(const string& item) const
+  {
+    auto iter = defaultmap.find(item);
+    return iter == defaultmap.end() ? "" : iter->second;
+  }
   using param_t = map<string, string>; //!< use this if you need to know the content of the map
 
   param_t::const_iterator begin(); //!< iterator semantics
   param_t::const_iterator end(); //!< iterator semantics
   const string& operator[](const string&); //!< iterator semantics
   const vector<string>& getCommands();
-  void gatherIncludes(std::vector<std::string>& extraConfigs);
+  void gatherIncludes(const std::string& dir, const std::string& suffix, std::vector<std::string>& extraConfigs);
+  void warnIfDeprecated(const string& var) const;
+  [[nodiscard]] static string isDeprecated(const string& var);
 #ifdef RECURSOR
   void setSLog(Logr::log_t log)
   {
@@ -125,7 +135,6 @@ public:
   }
 #endif
 private:
-  void warnIfDeprecated(const string& var);
   void parseOne(const string& arg, const string& parseOnly = "", bool lax = false);
   static string formatOne(bool running, bool full, const string& var, const string& help, const string& theDefault, const string& current);
   map<string, string> d_params;
index 70cde4a1c2167c9f8b67b8f4be47e9c157711a80..bb7e771610f7178e1346464fadd62b48daa5942f 100644 (file)
@@ -34,6 +34,7 @@
 /rec_control
 /pdns-recursor-*
 /recursor.conf-dist
+/recursor.yml-dist
 /ext/Makefile
 /ext/Makefile.in
 /dnsmessage.pb.cc
index fc330b3c6d2aef2a35c533b70bb9f589da1a4493..e231ea2b020366b4765cd089e7570019996d3542 100644 (file)
@@ -1,6 +1,7 @@
 JSON11_LIBS = $(top_srcdir)/ext/json11/libjson11.la
 PROBDS_LIBS = $(top_srcdir)/ext/probds/libprobds.la
 ARC4RANDOM_LIBS = $(top_srcdir)/ext/arc4random/libarc4random.la
+RUST_LIBS = $(top_srcdir)/settings/rust/libsettings.a $(LIBDL)
 
 AM_CPPFLAGS = $(LUA_CFLAGS) $(YAHTTP_CFLAGS) $(BOOST_CPPFLAGS) $(LIBSODIUM_CFLAGS) $(NET_SNMP_CFLAGS) $(LIBCAP_CFLAGS) $(SANITIZER_FLAGS) -O3 -Wall -pthread -DSYSCONFDIR=\"${sysconfdir}\" $(SYSTEMD_CFLAGS)
 
@@ -40,12 +41,13 @@ BUILT_SOURCES=htmlfiles.h \
        dnslabeltext.cc
 
 CLEANFILES = htmlfiles.h \
-       recursor.conf-dist
+       recursor.conf-dist recursor.yml-dist
 
 htmlfiles.h: incfiles html/* html/js/*
        ./incfiles > $@
 
-SUBDIRS=ext
+# We explicilt build settings in two steps, as settings modifies files in the settings/rust subdir
+SUBDIRS=ext settings settings/rust
 
 if LUA
 AM_CPPFLAGS +=$(LUA_CFLAGS)
@@ -192,6 +194,7 @@ pdns_recursor_SOURCES = \
        rpzloader.cc rpzloader.hh \
        secpoll-recursor.cc secpoll-recursor.hh \
        secpoll.cc secpoll.hh \
+       settings/cxxsupport.cc \
        sha.hh \
        sholder.hh \
        shuffle.cc shuffle.hh \
@@ -220,9 +223,12 @@ pdns_recursor_SOURCES = \
        zonemd.cc zonemd.hh \
        zoneparser-tng.cc zoneparser-tng.hh
 
+nodist_pdns_recursor_SOURCES = \
+       settings/cxxsettings-generated.cc
+
 if !HAVE_LUA_HPP
 BUILT_SOURCES += lua.hpp
-nodist_pdns_recursor_SOURCES = lua.hpp
+nodist_pdns_recursor_SOURCES += lua.hpp
 endif
 
 CLEANFILES += lua.hpp
@@ -238,7 +244,8 @@ pdns_recursor_LDADD = \
        $(BOOST_SYSTEM_LIBS) \
        $(PROBDS_LIBS) \
        $(LIBCAP_LIBS) \
-       $(ARC4RANDOM_LIBS)
+       $(ARC4RANDOM_LIBS) \
+       $(RUST_LIBS)
 
 pdns_recursor_LDFLAGS = $(AM_LDFLAGS) \
        $(LIBCRYPTO_LDFLAGS) $(BOOST_CONTEXT_LDFLAGS) \
@@ -252,7 +259,7 @@ pdns_recursor_LDFLAGS += \
        $(BOOST_FILESYSTEM_LDFLAGS)
 endif
 
-rec_control_LDADD = $(LIBCRYPTO_LIBS) $(ARC4RANDOM_LIBS)
+rec_control_LDADD = $(LIBCRYPTO_LIBS) $(ARC4RANDOM_LIBS) $(RUST_LIBS)
 
 rec_control_LDFLAGS = $(AM_LDFLAGS) \
        $(LIBCRYPTO_LDFLAGS)
@@ -309,6 +316,7 @@ testrunner_SOURCES = \
        root-dnssec.hh \
        rpzloader.cc rpzloader.hh \
        secpoll.cc \
+       settings/cxxsupport.cc \
        sholder.hh \
        sillyrecords.cc \
        sstuff.hh \
@@ -347,6 +355,7 @@ testrunner_SOURCES = \
        test-reczones-helpers.cc \
        test-rpzloader_cc.cc \
        test-secpoll_cc.cc \
+       test-settings.cc \
        test-signers.cc \
        test-syncres_cc.cc \
        test-syncres_cc.hh \
@@ -370,6 +379,9 @@ testrunner_SOURCES = \
        zonemd.cc zonemd.hh \
        zoneparser-tng.cc zoneparser-tng.hh
 
+nodist_testrunner_SOURCES = \
+       settings/cxxsettings-generated.cc
+
 testrunner_LDFLAGS = \
        $(AM_LDFLAGS) \
        $(BOOST_CONTEXT_LDFLAGS) \
@@ -385,7 +397,8 @@ testrunner_LDADD = \
        $(BOOST_SYSTEM_LIBS) \
        $(PROBDS_LIBS) \
        $(LIBCAP_LIBS) \
-       $(ARC4RANDOM_LIBS)
+       $(ARC4RANDOM_LIBS) \
+       $(RUST_LIBS)
 
 if NOD_ENABLED
 testrunner_SOURCES +=   nod.hh nod.cc \
@@ -497,8 +510,12 @@ rec_control_SOURCES = \
        qtype.cc \
        rec_channel.cc rec_channel.hh \
        rec_control.cc \
+       settings/cxxsupport.cc \
        unix_utility.cc
 
+nodist_rec_control_SOURCES = \
+       settings/cxxsettings-generated.cc
+
 dnslabeltext.cc: dnslabeltext.rl
        $(AM_V_GEN)$(RAGEL) $< -o dnslabeltext.cc
 
@@ -512,11 +529,14 @@ pubsuffix.cc: $(srcdir)/effective_tld_names.dat
        $(AM_V_GEN)./mkpubsuffixcc
 
 ## Config file
-sysconf_DATA = recursor.conf-dist
+sysconf_DATA = recursor.conf-dist recursor.yml-dist
 
 recursor.conf-dist: pdns_recursor
        $(AM_V_GEN)./pdns_recursor --config=default > $@
 
+recursor.yml-dist: pdns_recursor
+       dir=$$(mktemp -d) && touch "$$dir/recursor.yml" && ./pdns_recursor --config-dir="$$dir" --config=default 2> /dev/null > $@ && rm "$$dir/recursor.yml" && rmdir "$$dir"
+
 ## Manpages
 MANPAGES=pdns_recursor.1 \
         rec_control.1
index 2e8f9351b35e166b7529443b7f87ffd1fb0a3452..231f3ae7a2647ab6de37c5dc395dc88431ed1901 100644 (file)
@@ -35,7 +35,10 @@ AC_DEFINE([RECURSOR], [1],
 m4_pattern_forbid([^_?PKG_[A-Z_]+$], [*** pkg.m4 missing, please install pkg-config])
 
 AX_CXX_COMPILE_STDCXX_17([noext], [mandatory])
-LT_INIT()
+
+# Rust runtime used dlopen from its static lib
+LT_INIT([dlopen])
+AC_SUBST([LIBDL], [$lt_cv_dlopen_libs])
 
 PDNS_CHECK_OS
 PDNS_CHECK_NETWORK_LIBS
@@ -198,7 +201,9 @@ AC_CONFIG_FILES([Makefile
        ext/json11/Makefile
        ext/probds/Makefile
        ext/yahttp/Makefile
-       ext/yahttp/yahttp/Makefile])
+       ext/yahttp/yahttp/Makefile
+       settings/Makefile
+       settings/rust/Makefile])
 
 AC_OUTPUT
 
diff --git a/pdns/recursordist/docs/appendices/example/conversion b/pdns/recursordist/docs/appendices/example/conversion
new file mode 100644 (file)
index 0000000..f4b4d3d
--- /dev/null
@@ -0,0 +1,48 @@
+# THIS IS A PROOF OF CONCEPT! STRUCTURE, TYPES AND NAMES ARE SUBJECT TO CHANGE
+# Start of converted recursor.yml based on recursor.conf
+dnssec:
+  validation: validate
+incoming:
+  listen:
+  - 128.66.23.4:5353
+  - '[::1]:53'
+recursor:
+  forward_zones:
+  - zone: example
+    recurse: false
+    forwarders:
+    - 127.0.0.1:1024
+  forward_zones_file: fwzones.txt
+  include_dir: recursor.d
+# Validation result: OK
+# End of converted recursor.conf
+#
+# Found 1 .conf file in recursor.d
+# Converted include-dir recursor.d/01.conf to YAML format:
+incoming:
+  listen: !override
+  - 0.0.0.0
+recursor:
+  forward_zones:
+  - zone: example.com
+    recurse: false
+    forwarders:
+    - 128.66.9.99
+  - zone: example.net
+    recurse: false
+    forwarders:
+    - 128.66.9.100
+    - 128.66.9.101
+# Validation result: OK
+# End of converted recursor.d/01.conf
+#
+# Converted fwzones.txt to YAML format for recursor.forward_zones_file: 
+- zone: example.org
+  forwarders:
+  - 127.0.0.1:1024
+  recurse: true
+  notify_allowed: true
+# Validation result: OK
+# End of converted fwzones.txt
+#
+
diff --git a/pdns/recursordist/docs/appendices/example/fwzones.txt b/pdns/recursordist/docs/appendices/example/fwzones.txt
new file mode 100644 (file)
index 0000000..2c6f95a
--- /dev/null
@@ -0,0 +1 @@
+^+example.org=127.0.0.1:1024
\ No newline at end of file
diff --git a/pdns/recursordist/docs/appendices/example/generate.sh b/pdns/recursordist/docs/appendices/example/generate.sh
new file mode 100755 (executable)
index 0000000..ff91ec1
--- /dev/null
@@ -0,0 +1 @@
+../../../rec_control show-yaml recursor.conf > conversion
diff --git a/pdns/recursordist/docs/appendices/example/recursor.conf b/pdns/recursordist/docs/appendices/example/recursor.conf
new file mode 100644 (file)
index 0000000..4e82073
--- /dev/null
@@ -0,0 +1,5 @@
+dnssec=validate
+include-dir=recursor.d
+forward-zones = example=127.0.0.1:1024
+forward-zones-file=fwzones.txt
+local-address=128.66.23.4:5353, [::1]:53
diff --git a/pdns/recursordist/docs/appendices/example/recursor.d/01.conf b/pdns/recursordist/docs/appendices/example/recursor.d/01.conf
new file mode 100644 (file)
index 0000000..de62b20
--- /dev/null
@@ -0,0 +1,3 @@
+forward-zones += example.com=128.66.9.99
+forward-zones += example.net=128.66.9.100;128.66.9.101
+local-address = 0.0.0.0
diff --git a/pdns/recursordist/docs/appendices/yamlconversion.rst b/pdns/recursordist/docs/appendices/yamlconversion.rst
new file mode 100644 (file)
index 0000000..d846eb6
--- /dev/null
@@ -0,0 +1,61 @@
+Conversion of old-style settings to YAML format
+================================================
+
+Running the command
+
+.. code-block:: sh
+
+   rec_control show-yaml
+
+will show the conversion of existing old-style settings into the new YAML format.
+The existing settings will be read from the default old-style settings file ``recursor.conf`` in the configuration directory.
+It is also possible to show the conversion of a specific old-style settings file by running
+
+.. code-block:: sh
+
+   rec_control show-yaml path/to/recursor.conf
+
+``rec_control show-yaml`` will also show the conversions of any included ``.conf`` file (if :ref:`setting-include-dir` is set) and other associated settings file, like :ref:`setting-forward-zones-file`.
+
+Example
+-------
+
+Consider the old style configuration file ``recursor.conf``:
+
+.. literalinclude:: example/recursor.conf
+
+With the contents of ``recursor.d/01.conf``:
+
+.. literalinclude:: example/recursor.d/01.conf
+
+And ``fwzones.txt``:
+
+.. literalinclude:: example/fwzones.txt
+
+To show the conversion result, run:
+
+.. code-block:: sh
+
+   cd example
+   rec_control show-yaml recursor.conf
+
+Produces the following conversion report:
+
+.. literalinclude:: example/conversion
+
+Note  the ``!override`` tag for ``incoming.listen`` (corresponding to the ``=`` in ``recursor.d/01.conf``.
+The  ``recursor.forward_zones`` settings is extending the setting in the main ``recursord.yml`` file, as ``recursor.d/01.conf`` uses a ``+=`` for the ``forward-zones`` settings.
+Consult :doc:`../yamlsettings` for details on how settings spread over multiple files are merged.
+
+The contents of the report can be used to produce YAML settings equivalent to the old-style settings.
+This is a manual step and consists of copy-pasting the sections of the conversion report to individual files.
+Any settings filename in the include directory should end with ``.yml``.
+It is possible to put associated files (like a ``recursor.forward_zones_file``) into the include dir, but do *not* let these files end in ``.yml``, as ``.yml`` files will be interpreted as full configuration files, while the associated settings files are YAML sequences of a specific type.
+
+API Managed Files
+-----------------
+The format of API managed files was also changed to use YAML format.
+Specifically, the list of API managed zones is now a single file containing a sequence of ``auth_zones`` and a sequence of ``forward_zones`` instead of a settings file per zone.
+The list of ACLs is a YAML sequence of subnets or IP addresses.
+There is no conversion of API managed files from old-style to YAML yet.
+The intention is to add automatic conversion code before the final release of a YAML enabled :program:`Recursor`,
index 07140ec57c3a10f48894385796a686aa85f00288..cf56e82f98ba22a321db88c4f66bc20e26bc010c 100644 (file)
@@ -10,6 +10,7 @@ PowerDNS Recursor
     running
     dnssec
     settings
+    yamlsettings
     lua-config/index
     lua-scripting/index
     dns64
index 576393ab2e8edcc7583c5d8f20f4b5e82ff641f9..1200019244b39eccf657e5e32aa7d47f1173e0d5 100644 (file)
@@ -448,4 +448,4 @@ This function expects no argument and doesn't return any value.
         -- Perform here your maintenance
     end
 
-The interval can be configured through the :ref:`setting-maintenance-interval` setting.
+The interval can be configured through the :ref:`setting-lua-maintenance-interval` setting.
index 928694a12203042a14013e1a90cca375cee4d340..99b4a8317f2abf4603d67edb750d4e5e023e77af 100644 (file)
@@ -338,6 +338,9 @@ wipe-cache *DOMAIN* [*DOMAIN*] [...]
 wipe-cache-typed *qtype* *DOMAIN* [*DOMAIN*] [...]
     Same as wipe-cache, but only wipe records of type *qtype*.
 
+show-yaml [*FILE*]
+    Show Yaml representation of config. EXPERIMENTAL.
+
 See also
 --------
 :manpage:`pdns_recursor(1)`
index 85825b32bcb54db9d63ea5678373b7525153dab6..23682e1a01f82b18807d34078ddec68c200b8ae1 100644 (file)
@@ -1,8 +1,16 @@
+.. THIS IS A GENERATED FILE. DO NOT EDIT. SOURCE: see settings dir
+   START INCLUDE docs-old-preamble-in.rst
+
 PowerDNS Recursor Settings
 ==========================
 Each setting can appear on the command line, prefixed by ``--``, or in the configuration file.
 The command line overrides the configuration file.
 
+.. note::
+   Starting with version 5.0.0, :program:`Recursor` supports a new YAML syntax for configuration files.
+   A configuration using the old style syntax can be converted to a YAML configuration using the instructions in :doc:`appendices/yamlconversion`.
+   In a future release support for the "old-style" settings will be dropped.
+
 .. note::
    Settings marked as ``Boolean`` can either be set to an empty value, which means **on**, or to ``no`` or ``off`` which means **off**.
    Anything else means **on**.
@@ -30,26 +38,34 @@ For example::
 
 In this case the address ``128.66.1.2`` is excluded from the addresses allowed access.
 
+The Settings
+------------
+.. END INCLUDE docs-old-preamble-in.rst
+
 .. _setting-aggressive-nsec-cache-size:
 
 ``aggressive-nsec-cache-size``
-------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Integer
 -  Default: 100000
 
+- YAML setting: :ref:`setting-yaml-dnssec.aggressive_nsec_cache_size`
+
 The number of records to cache in the aggressive cache. If set to a value greater than 0, the recursor will cache NSEC and NSEC3 records to generate negative answers, as defined in :rfc:`8198`.
-To use this, DNSSEC processing or validation must be enabled by setting `dnssec`_ to ``process``, ``log-fail`` or ``validate``.
+To use this, DNSSEC processing or validation must be enabled by setting :ref:`setting-dnssec` to ``process``, ``log-fail`` or ``validate``.
 
 .. _setting-aggressive-cache-min-nsec3-hit-ratio:
 
 ``aggressive-cache-min-nsec3-hit-ratio``
-----------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.9.0
 
-- Integer
-- Default: 2000
+-  Integer
+-  Default: 2000
+
+- YAML setting: :ref:`setting-yaml-dnssec.aggressive_cache_min_nsec3_hit_ratio`
 
 The limit for which to put NSEC3 records into the aggressive cache.
 A value of ``n`` means that an NSEC3 record is only put into the aggressive cache if the estimated probability of a random name hitting the NSEC3 record is higher than ``1/n``.
@@ -62,38 +78,47 @@ This setting avoids doing unnecessary work for such large zones.
 .. _setting-allow-from:
 
 ``allow-from``
---------------
--  IP addresses or netmasks, separated by commas, negation supported
+~~~~~~~~~~~~~~
+
+-  Comma separated list of IP addresses or subnets, negation supported
 -  Default: 127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10
 
+- YAML setting: :ref:`setting-yaml-incoming.allow_from`
+
 Netmasks (both IPv4 and IPv6) that are allowed to use the server.
 The default allows access only from :rfc:`1918` private IP addresses.
 An empty value means no checking is done, all clients are allowed.
 Due to the aggressive nature of the internet these days, it is highly recommended to not open up the recursor for the entire internet.
 Questions from IP addresses not listed here are ignored and do not get an answer.
 
-When the Proxy Protocol is enabled (see `proxy-protocol-from`_), the recursor will check the address of the client IP advertised in the Proxy Protocol header instead of the one of the proxy.
+When the Proxy Protocol is enabled (see :ref:`setting-proxy-protocol-from`), the recursor will check the address of the client IP advertised in the Proxy Protocol header instead of the one of the proxy.
 
 Note that specifying an IP address without a netmask uses an implicit netmask of /32 or /128.
 
 .. _setting-allow-from-file:
 
 ``allow-from-file``
--------------------
--  Path
+~~~~~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-incoming.allow_from_file`
 
-Like `allow-from`_, except reading from file.
-Overrides the `allow-from`_ setting. To use this feature, supply one netmask per line, with optional comments preceded by a "#".
+Like :ref:`setting-allow-from`, except reading from file.
+Overrides the :ref:`setting-allow-from` setting. To use this feature, supply one netmask per line, with optional comments preceded by a '#'.
 
 .. _setting-allow-notify-for:
 
 ``allow-notify-for``
----------------------
+~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
--  Comma separated list of domain-names
+-  Comma separated list of strings
 -  Default: (empty)
 
+- YAML setting: :ref:`setting-yaml-incoming.allow_notify_for`
+
 Domain names specified in this list are used to permit incoming
 NOTIFY operations to wipe any cache entries that match the domain
 name. If this list is empty, all NOTIFY operations will be ignored.
@@ -101,31 +126,36 @@ name. If this list is empty, all NOTIFY operations will be ignored.
 .. _setting-allow-notify-for-file:
 
 ``allow-notify-for-file``
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
--  Path
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-incoming.allow_notify_for_file`
 
-Like `allow-notify-for`_, except reading from file. To use this
+Like :ref:`setting-allow-notify-for`, except reading from file. To use this
 feature, supply one domain name per line, with optional comments
-preceded by a "#".
+preceded by a '#'.
 
-NOTIFY-allowed zones can also be specified using `forward-zones-file`_.
+NOTIFY-allowed zones can also be specified using :ref:`setting-forward-zones-file`.
 
 .. _setting-allow-notify-from:
 
 ``allow-notify-from``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
--  IP addresses or netmasks, separated by commas, negation supported
--  Default: unset
+-  Comma separated list of IP addresses or subnets, negation supported
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-incoming.allow_notify_from`
 
 Netmasks (both IPv4 and IPv6) that are allowed to issue NOTIFY operations
 to the server.  NOTIFY operations from IP addresses not listed here are
 ignored and do not get an answer.
 
-When the Proxy Protocol is enabled (see `proxy-protocol-from`_), the
+When the Proxy Protocol is enabled (see :ref:`setting-proxy-protocol-from`), the
 recursor will check the address of the client IP advertised in the
 Proxy Protocol header instead of the one of the proxy.
 
@@ -135,50 +165,60 @@ netmask of /32 or /128.
 NOTIFY operations received from a client listed in one of these netmasks
 will be accepted and used to wipe any cache entries whose zones match
 the zone specified in the NOTIFY operation, but only if that zone (or
-one of its parents) is included in `allow-notify-for`_,
-`allow-notify-for-file`_, or `forward-zones-file`_ with a '^' prefix.
+one of its parents) is included in :ref:`setting-allow-notify-for`,
+:ref:`setting-allow-notify-for-file`, or :ref:`setting-forward-zones-file` with a '^' prefix.
 
 .. _setting-allow-notify-from-file:
 
 ``allow-notify-from-file``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
--  Path
+-  String
+-  Default: (empty)
 
-Like `allow-notify-from`_, except reading from file. To use this
+- YAML setting: :ref:`setting-yaml-incoming.allow_notify_from_file`
+
+Like :ref:`setting-allow-notify-from`, except reading from file. To use this
 feature, supply one netmask per line, with optional comments preceded
-by a "#".
+by a '#'.
 
 .. _setting-any-to-tcp:
 
 ``any-to-tcp``
---------------
+~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-recursor.any_to_tcp`
+
 Answer questions for the ANY type on UDP with a truncated packet that refers the remote server to TCP.
 Useful for mitigating ANY reflection attacks.
 
 .. _setting-allow-trust-anchor-query:
 
 ``allow-trust-anchor-query``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.3.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-recursor.allow_trust_anchor_query`
+
 Allow ``trustanchor.server CH TXT`` and ``negativetrustanchor.server CH TXT`` queries to view the configured :doc:`DNSSEC <dnssec>` (negative) trust anchors.
 
 .. _setting-api-config-dir:
 
 ``api-config-dir``
-------------------
+~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.0.0
 
--  Path
--  Default: unset
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-webservice.api_dir`
 
 Directory where the REST API stores its configuration and zones.
 For configuration updates to work, :ref:`setting-include-dir` should have the same value.
@@ -186,45 +226,28 @@ For configuration updates to work, :ref:`setting-include-dir` should have the sa
 .. _setting-api-key:
 
 ``api-key``
------------
+~~~~~~~~~~~
 .. versionadded:: 4.0.0
 .. versionchanged:: 4.6.0
+
   This setting now accepts a hashed and salted version.
 
 -  String
--  Default: unset
-
-Static pre-shared authentication key for access to the REST API. Since 4.6.0 the key can be hashed and salted using ``rec_control hash-password`` instead of being stored in the configuration in plaintext, but the plaintext version is still supported.
-
-.. _setting-api-readonly:
-
-``api-readonly``
-----------------
-.. versionchanged:: 4.2.0
-  This setting has been removed.
-
--  Boolean
--  Default: no
-
-Disallow data modification through the REST API when set.
-
-.. _setting-api-logfile:
-
-``api-logfile``
----------------
-.. versionchanged:: 4.2.0
-  This setting has been removed.
+-  Default: (empty)
 
--  Path
--  Default: unset
+- YAML setting: :ref:`setting-yaml-webservice.api_key`
 
-Location of the server logfile (used by the REST API).
+Static pre-shared authentication key for access to the REST API. Since 4.6.0 the key can be hashed and salted using ``rec_control hash-password`` instead of being stored in the configuration in plaintext, but the plaintext version is still supported.
 
 .. _setting-auth-zones:
 
 ``auth-zones``
---------------
+~~~~~~~~~~~~~~
+
 -  Comma separated list of 'zonename=filename' pairs
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.auth_zones`
 
 Zones read from these files (in BIND format) are served authoritatively (but without the AA bit set in responses).
 DNSSEC is not supported. Example:
@@ -236,28 +259,38 @@ DNSSEC is not supported. Example:
 .. _setting-carbon-interval:
 
 ``carbon-interval``
--------------------
+~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 30
 
+- YAML setting: :ref:`setting-yaml-carbon.interval`
+
 If sending carbon updates, this is the interval between them in seconds.
 See :doc:`metrics`.
 
 .. _setting-carbon-namespace:
 
 ``carbon-namespace``
---------------------
+~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  String
+-  Default: pdns
+
+- YAML setting: :ref:`setting-yaml-carbon.ns`
 
 Change the namespace or first string of the metric key. The default is pdns.
 
 .. _setting-carbon-ourname:
 
 ``carbon-ourname``
-------------------
+~~~~~~~~~~~~~~~~~~
+
 -  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-carbon.ourname`
 
 If sending carbon updates, if set, this will override our hostname.
 Be careful not to include any dots in this setting, unless you know what you are doing.
@@ -266,18 +299,25 @@ See :ref:`metricscarbon`.
 .. _setting-carbon-instance:
 
 ``carbon-instance``
---------------------
+~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  String
+-  Default: recursor
+
+- YAML setting: :ref:`setting-yaml-carbon.instance`
 
 Change the instance or third string of the metric key. The default is recursor.
 
 .. _setting-carbon-server:
 
 ``carbon-server``
------------------
--  IP address
+~~~~~~~~~~~~~~~~~
+
+-  Comma separated list or IPs of IP:port combinations
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-carbon.server`
 
 If set to an IP or IPv6 address, will send all available metrics to this server via the carbon protocol, which is used by graphite and metronome. Moreover you can specify more than one server using a comma delimited list, ex: carbon-server=10.10.10.10,10.10.10.20.
 You may specify an alternate port by appending :port, for example: ``127.0.0.1:2004``.
@@ -286,8 +326,12 @@ See :doc:`metrics`.
 .. _setting-chroot:
 
 ``chroot``
-----------
--  Path to a Directory
+~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.chroot`
 
 If set, chroot to this directory for more security.
 This is not recommended; instead, we recommend containing PowerDNS using operating system features.
@@ -296,9 +340,7 @@ We ship systemd unit files with our packages to make this easy.
 Make sure that ``/dev/log`` is available from within the chroot.
 Logging will silently fail over time otherwise (on logrotate).
 
-When using ``chroot``, all other paths (except for `config-dir`_) set in the configuration are relative to the new root.
-
-When using ``chroot`` and the API (`webserver`_), `api-readonly`_ **must** be set and `api-config-dir`_ unset.
+When using ``chroot``, all other paths (except for :ref:`setting-config-dir`) set in the configuration are relative to the new root.
 
 When running on a system where systemd manages services, ``chroot`` does not work out of the box, as PowerDNS cannot use the ``NOTIFY_SOCKET``.
 Either do not ``chroot`` on these systems or set the 'Type' of this service to 'simple' instead of 'notify' (refer to the systemd documentation on how to modify unit-files).
@@ -306,17 +348,24 @@ Either do not ``chroot`` on these systems or set the 'Type' of this service to '
 .. _setting-client-tcp-timeout:
 
 ``client-tcp-timeout``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 2
 
+- YAML setting: :ref:`setting-yaml-incoming.tcp_timeout`
+
 Time to wait for data from TCP clients.
 
 .. _setting-config-dir:
 
 ``config-dir``
---------------
--  Path
+~~~~~~~~~~~~~~
+
+-  String
+-  Default: /etc/powerdns
+
+- YAML setting: :ref:`setting-yaml-recursor.config_dir`
 
 Location of configuration directory (``recursor.conf``).
 Usually ``/etc/powerdns``, but this depends on ``SYSCONFDIR`` during compile-time.
@@ -324,25 +373,33 @@ Usually ``/etc/powerdns``, but this depends on ``SYSCONFDIR`` during compile-tim
 .. _setting-config-name:
 
 ``config-name``
----------------
+~~~~~~~~~~~~~~~
+
 -  String
--  Default: unset
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.config_name`
 
 When running multiple recursors on the same server, read settings from :file:`recursor-{name}.conf`, this will also rename the binary image.
 
 .. _setting-cpu-map:
 
 ``cpu-map``
------------
+~~~~~~~~~~~
 
-- String
-- Default: unset
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.cpu_map`
 
 Set CPU affinity for threads, asking the scheduler to run those threads on a single CPU, or a set of CPUs.
 This parameter accepts a space separated list of thread-id=cpu-id, or thread-id=cpu-id-1,cpu-id-2,...,cpu-id-N.
 For example, to make the worker thread 0 run on CPU id 0 and the worker thread 1 on CPUs 1 and 2::
 
-  cpu-map=0=0 1=1,2
+.. code-block:: yaml
+
+  recursor:
+    cpu_map: 0=0 1=1,2
 
 The thread handling the control channel, the webserver and other internal stuff has been assigned id 0, the distributor
 threads if any are assigned id 1 and counting, and the worker threads follow behind.
@@ -357,25 +414,29 @@ These threads cannot be specified in this setting as their thread-ids are left u
 .. _setting-daemon:
 
 ``daemon``
-----------
+~~~~~~~~~~
+.. versionchanged:: 4.0.0
+
+  Default is now ``no``, was ``yes`` before.
+
 -  Boolean
 -  Default: no
 
-.. versionchanged:: 4.0.0
-
-    Default is now "no", was "yes" before.
+- YAML setting: :ref:`setting-yaml-recursor.daemon`
 
 Operate in the background.
 
 .. _setting-dont-throttle-names:
 
 ``dont-throttle-names``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
--  Comma separated list of domain-names
+-  Comma separated list of strings
 -  Default: (empty)
 
+- YAML setting: :ref:`setting-yaml-outgoing.dont_throttle_names`
+
 When an authoritative server does not answer a query or sends a reply the recursor does not like, it is throttled.
 Any servers' name suffix-matching the supplied names will never be throttled.
 
@@ -385,17 +446,19 @@ Any servers' name suffix-matching the supplied names will never be throttled.
 .. _setting-dont-throttle-netmasks:
 
 ``dont-throttle-netmasks``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
--  Comma separated list of netmasks, negation not supported
+-  Comma separated list of IP addresses or subnets, negation supported
 -  Default: (empty)
 
+- YAML setting: :ref:`setting-yaml-outgoing.dont_throttle_netmasks`
+
 When an authoritative server does not answer a query or sends a reply the recursor does not like, it is throttled.
 Any servers matching the supplied netmasks will never be throttled.
 
 This can come in handy on lossy networks when forwarding, where the same server is configured multiple times (e.g. with ``forward-zones-recurse=example.com=192.0.2.1;192.0.2.1``).
-By default, the PowerDNS Recursor would throttle the "first" server on a timeout and hence not retry the "second" one.
+By default, the PowerDNS Recursor would throttle the 'first' server on a timeout and hence not retry the 'second' one.
 In this case, ``dont-throttle-netmasks`` could be set to ``192.0.2.1``.
 
 .. warning::
@@ -404,33 +467,41 @@ In this case, ``dont-throttle-netmasks`` could be set to ``192.0.2.1``.
 .. _setting-disable-packetcache:
 
 ``disable-packetcache``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-packetcache.disable`
+
 Turn off the packet cache. Useful when running with Lua scripts that can not be cached, though individual query caching can be controlled from Lua as well.
 
 .. _setting-disable-syslog:
 
 ``disable-syslog``
-------------------
+~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-logging.disable_syslog`
+
 Do not log to syslog, only to stdout.
 Use this setting when running inside a supervisor that handles logging (like systemd).
-**Note**: do not use this setting in combination with `daemon`_ as all logging will disappear.
+**Note**: do not use this setting in combination with :ref:`setting-daemon` as all logging will disappear.
 
 .. _setting-distribution-load-factor:
 
 ``distribution-load-factor``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.12
 
 -  Double
 -  Default: 0.0
 
-If `pdns-distributes-queries`_ is set and this setting is set to another value
+- YAML setting: :ref:`setting-yaml-incoming.distribution_load_factor`
+
+If :ref:`setting-pdns-distributes-queries` is set and this setting is set to another value
 than 0, the distributor thread will use a bounded load-balancing algorithm while
 distributing queries to worker threads, making sure that no thread is assigned
 more queries than distribution-load-factor times the average number of queries
@@ -443,12 +514,14 @@ number of requests asking for the same qname.
 .. _setting-distribution-pipe-buffer-size:
 
 ``distribution-pipe-buffer-size``
----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  Integer
 -  Default: 0
 
+- YAML setting: :ref:`setting-yaml-incoming.distribution_pipe_buffer_size`
+
 Size in bytes of the internal buffer of the pipe used by the distributor to pass incoming queries to a worker thread.
 Requires support for `F_SETPIPE_SZ` which is present in Linux since 2.6.35. The actual size might be rounded up to
 a multiple of a page size. 0 means that the OS default size is used.
@@ -458,23 +531,27 @@ overloaded, but it will be at the cost of an increased latency.
 .. _setting-distributor-threads:
 
 ``distributor-threads``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  Integer
--  Default: 1 if `pdns-distributes-queries`_ is set, 0 otherwise
+-  Default: 1 if :ref:`setting-pdns-distributes-queries` is set, 0 otherwise
 
-If `pdns-distributes-queries`_ is set, spawn this number of distributor threads on startup. Distributor threads
+- YAML setting: :ref:`setting-yaml-incoming.distributor_threads`
+
+If :ref:`setting-pdns-distributes-queries` is set, spawn this number of distributor threads on startup. Distributor threads
 handle incoming queries and distribute them to other threads based on a hash of the query.
 
 .. _setting-dot-to-auth-names:
 
 ``dot-to-auth-names``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
-- Comma separated list of domain-names or suffixes
-- Default: (empty).
+-  Comma separated list of strings
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-outgoing.dot_to_auth_names`
 
 Force DoT to the listed authoritative nameservers. For this to work, DoT support has to be compiled in.
 Currently, the certificate is not checked for validity in any way.
@@ -482,22 +559,26 @@ Currently, the certificate is not checked for validity in any way.
 .. _setting-dot-to-port-853:
 
 ``dot-to-port-853``
--------------------
+~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
-- Boolean
-- Default: ``yes`` if DoT support is compiled in, ``no`` otherwise.
+-  Boolean
+-  Default: yes
+
+- YAML setting: :ref:`setting-yaml-outgoing.dot_to_port_853`
 
 Enable DoT to forwarders that specify port 853.
 
 .. _setting-dns64-prefix:
 
 ``dns64-prefix``
-----------------
+~~~~~~~~~~~~~~~~
 .. versionadded:: 4.4.0
 
--  Netmask, as a string
--  Default: None
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.dns64_prefix`
 
 Enable DNS64 (:rfc:`6147`) support using the supplied /96 IPv6 prefix. This will generate 'fake' ``AAAA`` records for names
 with only ``A`` records, as well as 'fake' ``PTR`` records to make sure that reverse lookup of DNS64-generated IPv6 addresses
@@ -507,14 +588,18 @@ See :doc:`dns64` for more flexible but slower alternatives using Lua.
 .. _setting-dnssec:
 
 ``dnssec``
-----------
+~~~~~~~~~~
 .. versionadded:: 4.0.0
-
 .. versionchanged:: 4.5.0
-   The default changed from ``process-no-validate`` to ``process``
 
--  One of ``off``, ``process-no-validate``, ``process``, ``log-fail``, ``validate``, String
--  Default: ``process``
+  The default changed from ``process-no-validate`` to ``process``
+
+-  String
+-  Default: process
+
+- YAML setting: :ref:`setting-yaml-dnssec.validation`
+
+One of ``off``, ``process-no-validate``, ``process``, ``log-fail``, ``validate``
 
 Set the mode for DNSSEC processing, as detailed in :doc:`dnssec`.
 
@@ -536,11 +621,13 @@ Set the mode for DNSSEC processing, as detailed in :doc:`dnssec`.
 .. _setting-dnssec-disabled-algorithms:
 
 ``dnssec-disabled-algorithms``
-------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.9.0
 
-- Comma separated list of DNSSEC algorithm numbers
-- Default: (none)
+-  Comma separated list of strings
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-dnssec.disabled_algorithms`
 
 A list of DNSSEC algorithm numbers that should be considered disabled.
 These algorithms will not be used to validate DNSSEC signatures.
@@ -555,123 +642,146 @@ On such systems not disabling some algorithms (or changing the security policy)
 .. _setting-dnssec-log-bogus:
 
 ``dnssec-log-bogus``
---------------------
+~~~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-dnssec.log_bogus`
+
 Log every DNSSEC validation failure.
 **Note**: This is not logged per-query but every time records are validated as Bogus.
 
 .. _setting-dont-query:
 
 ``dont-query``
---------------
--  Netmasks, comma separated, negation supported
+~~~~~~~~~~~~~~
+
+-  Comma separated list of IP addresses or subnets, negation supported
 -  Default: 127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10, 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 240.0.0.0/4, ::/96, ::ffff:0:0/96, 100::/64, 2001:db8::/32
 
+- YAML setting: :ref:`setting-yaml-outgoing.dont_query`
+
 The DNS is a public database, but sometimes contains delegations to private IP addresses, like for example 127.0.0.1.
 This can have odd effects, depending on your network, and may even be a security risk.
 Therefore, the PowerDNS Recursor by default does not query private space IP addresses.
 This setting can be used to expand or reduce the limitations.
 
-Queries for names in forward zones and to addresses as configured in any of the settings `forward-zones`_, `forward-zones-file`_ or `forward-zones-recurse`_ are performed regardless of these limitations.
+Queries for names in forward zones and to addresses as configured in any of the settings :ref:`setting-forward-zones`, :ref:`setting-forward-zones-file` or :ref:`setting-forward-zones-recurse` are performed regardless of these limitations.
 
 .. _setting-ecs-add-for:
 
 ``ecs-add-for``
----------------
+~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
--  Comma separated list of netmasks, negation supported
+-  Comma separated list of IP addresses or subnets, negation supported
 -  Default: 0.0.0.0/0, ::/0, !127.0.0.0/8, !10.0.0.0/8, !100.64.0.0/10, !169.254.0.0/16, !192.168.0.0/16, !172.16.0.0/12, !::1/128, !fc00::/7, !fe80::/10
 
-List of requestor netmasks for which the requestor IP Address should be used as the :rfc:`EDNS Client Subnet <7871>` for outgoing queries. Outgoing queries for requestors that do not match this list will use the `ecs-scope-zero-address`_ instead.
-Valid incoming ECS values from `use-incoming-edns-subnet`_ are not replaced.
+- YAML setting: :ref:`setting-yaml-ecs.add_for`
 
-Regardless of the value of this setting, ECS values are only sent for outgoing queries matching the conditions in the `edns-subnet-allow-list`_ setting. This setting only controls the actual value being sent.
+List of requestor netmasks for which the requestor IP Address should be used as the :rfc:`EDNS Client Subnet <7871>` for outgoing queries. Outgoing queries for requestors that do not match this list will use the :ref:`setting-ecs-scope-zero-address` instead.
+Valid incoming ECS values from :ref:`setting-use-incoming-edns-subnet` are not replaced.
 
-This defaults to not using the requestor address inside RFC1918 and similar "private" IP address spaces.
+Regardless of the value of this setting, ECS values are only sent for outgoing queries matching the conditions in the :ref:`setting-edns-subnet-allow-list` setting. This setting only controls the actual value being sent.
+
+This defaults to not using the requestor address inside RFC1918 and similar 'private' IP address spaces.
 
 .. _setting-ecs-ipv4-bits:
 
 ``ecs-ipv4-bits``
------------------
+~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Integer
 -  Default: 24
 
+- YAML setting: :ref:`setting-yaml-ecs.ipv4_bits`
+
 Number of bits of client IPv4 address to pass when sending EDNS Client Subnet address information.
 
 .. _setting-ecs-ipv4-cache-bits:
 
 ``ecs-ipv4-cache-bits``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.12
 
 -  Integer
 -  Default: 24
 
+- YAML setting: :ref:`setting-yaml-ecs.ipv4_cache_bits`
+
 Maximum number of bits of client IPv4 address used by the authoritative server (as indicated by the EDNS Client Subnet scope in the answer) for an answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-cache-limit-ttl``.
 That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
 
 .. _setting-ecs-ipv6-bits:
 
 ``ecs-ipv6-bits``
------------------
+~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Integer
 -  Default: 56
 
+- YAML setting: :ref:`setting-yaml-ecs.ipv6_bits`
+
 Number of bits of client IPv6 address to pass when sending EDNS Client Subnet address information.
 
 .. _setting-ecs-ipv6-cache-bits:
 
 ``ecs-ipv6-cache-bits``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.12
 
 -  Integer
 -  Default: 56
 
+- YAML setting: :ref:`setting-yaml-ecs.ipv6_cache_bits`
+
 Maximum number of bits of client IPv6 address used by the authoritative server (as indicated by the EDNS Client Subnet scope in the answer) for an answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-cache-limit-ttl``.
 That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
 
 .. _setting-ecs-ipv4-never-cache:
 
 ``ecs-ipv4-never-cache``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-ecs.ipv4_never_cache`
+
 When set, never cache replies carrying EDNS IPv4 Client Subnet scope in the record cache.
 In this case the decision made by ```ecs-ipv4-cache-bits`` and ``ecs-cache-limit-ttl`` is no longer relevant.
 
 .. _setting-ecs-ipv6-never-cache:
 
 ``ecs-ipv6-never-cache``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-ecs.ipv6_never_cache`
+
 When set, never cache replies carrying EDNS IPv6 Client Subnet scope in the record cache.
 In this case the decision made by ```ecs-ipv6-cache-bits`` and ``ecs-cache-limit-ttl`` is no longer relevant.
 
 .. _setting-ecs-minimum-ttl-override:
 
 ``ecs-minimum-ttl-override``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.5.0
+
   Old versions used default 0.
 
 -  Integer
 -  Default: 1
 
+- YAML setting: :ref:`setting-yaml-ecs.minimum_ttl_override`
+
 This setting artificially raises the TTLs of records in the ANSWER section of ECS-specific answers to be at least this long.
 Setting this to a value greater than 1 technically is an RFC violation, but might improve performance a lot.
 Using a value of 0 impacts performance of TTL 0 records greatly, since it forces the recursor to contact
@@ -681,11 +791,13 @@ Can be set at runtime using ``rec_control set-ecs-minimum-ttl 3600``.
 .. _setting-ecs-cache-limit-ttl:
 
 ``ecs-cache-limit-ttl``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.12
 
 -  Integer
--  Default: 0 (disabled)
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-ecs.cache_limit_ttl`
 
 The minimum TTL for an ECS-specific answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-ipv4-cache-bits`` or ``ecs-ipv6-cache-bits``.
 That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
@@ -693,29 +805,34 @@ That is, only if both the limits apply, the record will not be cached. This deci
 .. _setting-ecs-scope-zero-address:
 
 ``ecs-scope-zero-address``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
-- IPv4 or IPv6 Address
-- Default: empty
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-ecs.scope_zero_address`
 
 The IP address sent via EDNS Client Subnet to authoritative servers listed in
-`edns-subnet-allow-list`_ when `use-incoming-edns-subnet`_ is set and the query has
+:ref:`setting-edns-subnet-allow-list` when :ref:`setting-use-incoming-edns-subnet` is set and the query has
 an ECS source prefix-length set to 0.
 The default is to look for the first usable (not an ``any`` one) address in
-`query-local-address`_ (starting with IPv4). If no suitable address is
+:ref:`setting-query-local-address` (starting with IPv4). If no suitable address is
 found, the recursor fallbacks to sending 127.0.0.1.
 
 .. _setting-edns-outgoing-bufsize:
 
 ``edns-outgoing-bufsize``
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.2.0
+
   Before 4.2.0, the default was 1680
 
 -  Integer
 -  Default: 1232
 
+- YAML setting: :ref:`setting-yaml-outgoing.edns_bufsize`
+
 .. note:: Why 1232?
 
   1232 is the largest number of payload bytes that can fit in the smallest IPv6 packet.
@@ -727,69 +844,89 @@ Lower this if you experience timeouts.
 .. _setting-edns-padding-from:
 
 ``edns-padding-from``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  Comma separated list of netmasks, negation supported
--  Default: (none)
+-  String
+-  Default: (empty)
 
-List of netmasks (proxy IP in case of proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses, provided that `edns-padding-mode`_ applies.
+- YAML setting: :ref:`setting-yaml-incoming.edns_padding_from`
+
+List of netmasks (proxy IP in case of proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses, provided that :ref:`setting-edns-padding-mode` applies.
 
 .. _setting-edns-padding-mode:
 
 ``edns-padding-mode``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  One of ``always``, ``padded-queries-only``, String
--  Default: ``padded-queries-only``
+-  String
+-  Default: padded-queries-only
+
+- YAML setting: :ref:`setting-yaml-incoming.edns_padding_mode`
 
+One of ``always``, ``padded-queries-only``.
 Whether to add EDNS padding to all responses (``always``) or only to responses for queries containing the EDNS padding option (``padded-queries-only``, the default).
-In both modes, padding will only be added to responses for queries coming from `edns-padding-from`_ sources.
+In both modes, padding will only be added to responses for queries coming from :ref:`setting-edns-padding-from` sources.
 
 .. _setting-edns-padding-out:
 
 ``edns-padding-out``
---------------------
+~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.8.0
 
-- Boolean
-- Default: yes
+-  Boolean
+-  Default: yes
+
+- YAML setting: :ref:`setting-yaml-outgoing.edns_padding`
 
 Whether to add EDNS padding to outgoing DoT queries.
 
 .. _setting-edns-padding-tag:
 
 ``edns-padding-tag``
---------------------
+~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Integer
 -  Default: 7830
 
-The packetcache tag to use for padded responses, to prevent a client not allowed by the `edns-padding-from`_ list to be served a cached answer generated for an allowed one. This
-effectively divides the packet cache in two when `edns-padding-from`_ is used. Note that this will not override a tag set from one of the ``Lua`` hooks.
+- YAML setting: :ref:`setting-yaml-incoming.edns_padding_tag`
+
+The packetcache tag to use for padded responses, to prevent a client not allowed by the :ref::`setting-edns-padding-from` list to be served a cached answer generated for an allowed one. This
+effectively divides the packet cache in two when :ref:`setting-edns-padding-from` is used. Note that this will not override a tag set from one of the ``Lua`` hooks.
 
 .. _setting-edns-subnet-whitelist:
 
 ``edns-subnet-whitelist``
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. deprecated:: 4.5.0
- Use :ref:`setting-edns-subnet-allow-list`.
+
+  Use :ref:`setting-edns-subnet-allow-list`.
+
+-  String
+-  Default: (empty)
+
+- YAML setting does not exist
+
+
 
 .. _setting-edns-subnet-allow-list:
 
 ``edns-subnet-allow-list``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  Comma separated list of domain names and netmasks, negation supported
--  Default: (none)
+-  Comma separated list of strings
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-outgoing.edns_subnet_allow_list`
 
 List of netmasks and domains that :rfc:`EDNS Client Subnet <7871>` should be enabled for in outgoing queries.
 
-For example, an EDNS Client Subnet option containing the address of the initial requestor (but see `ecs-add-for`_) will be added to an outgoing query sent to server 192.0.2.1 for domain X if 192.0.2.1 matches one of the supplied netmasks, or if X matches one of the supplied domains.
-The initial requestor address will be truncated to 24 bits for IPv4 (see `ecs-ipv4-bits`_) and to 56 bits for IPv6 (see `ecs-ipv6-bits`_), as recommended in the privacy section of RFC 7871.
+For example, an EDNS Client Subnet option containing the address of the initial requestor (but see :ref:`setting-ecs-add-for`) will be added to an outgoing query sent to server 192.0.2.1 for domain X if 192.0.2.1 matches one of the supplied netmasks, or if X matches one of the supplied domains.
+The initial requestor address will be truncated to 24 bits for IPv4 (see :ref:`setting-ecs-ipv4-bits`) and to 56 bits for IPv6 (see :ref:`setting-ecs-ipv6-bits`), as recommended in the privacy section of RFC 7871.
+
 
 Note that this setting describes the destination of outgoing queries, not the sources of incoming queries, nor the subnets described in the EDNS Client Subnet option.
 
@@ -798,10 +935,13 @@ By default, this option is empty, meaning no EDNS Client Subnet information is s
 .. _setting-entropy-source:
 
 ``entropy-source``
-------------------
--  Path
+~~~~~~~~~~~~~~~~~~
+
+-  String
 -  Default: /dev/urandom
 
+- YAML setting: :ref:`setting-yaml-recursor.entropy_source`
+
 PowerDNS can read entropy from a (hardware) source.
 This is used for generating random numbers which are very hard to predict.
 Generally on UNIX platforms, this source will be ``/dev/urandom``, which will always supply random numbers, even if entropy is lacking.
@@ -810,21 +950,26 @@ Change to ``/dev/random`` if PowerDNS should block waiting for enough entropy to
 .. _setting-etc-hosts-file:
 
 ``etc-hosts-file``
-------------------
--  Path
+~~~~~~~~~~~~~~~~~~
+
+-  String
 -  Default: /etc/hosts
 
+- YAML setting: :ref:`setting-yaml-recursor.etc_hosts_file`
+
 The path to the /etc/hosts file, or equivalent.
-This file can be used to serve data authoritatively using `export-etc-hosts`_.
+This file can be used to serve data authoritatively using :ref:`setting-export-etc-hosts`.
 
 .. _setting-event-trace-enabled:
 
 ``event-trace-enabled``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
-- Integer
-- Default: 0
+-  Integer
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-recursor.event_trace_enabled`
 
 Enable the recording and logging of ref:`event traces`. This is an experimental feature and subject to change.
 Possible values are 0: (disabled), 1 (add information to protobuf logging messages) and 2 (write to log) and 3 (both).
@@ -832,38 +977,51 @@ Possible values are 0: (disabled), 1 (add information to protobuf logging messag
 .. _setting-export-etc-hosts:
 
 ``export-etc-hosts``
---------------------
+~~~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-recursor.export_etc_hosts`
+
 If set, this flag will export the host names and IP addresses mentioned in ``/etc/hosts``.
 
 .. _setting-export-etc-hosts-search-suffix:
 
 ``export-etc-hosts-search-suffix``
-----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  String
+-  Default: (empty)
 
-If set, all hostnames in the `export-etc-hosts`_ file are loaded in canonical form, based on this suffix, unless the name contains a '.', in which case the name is unchanged.
+- YAML setting: :ref:`setting-yaml-recursor.export_etc_hosts_search_suffix`
+
+If set, all hostnames in the :ref:`setting-export-etc-hosts` file are loaded in canonical form, based on this suffix, unless the name contains a '.', in which case the name is unchanged.
 So an entry called 'pc' with ``export-etc-hosts-search-suffix='home.com'`` will lead to the generation of 'pc.home.com' within the recursor.
 An entry called 'server1.home' will be stored as 'server1.home', regardless of this setting.
 
 .. _setting-extended-resolution-errors:
 
 ``extended-resolution-errors``
-------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-recursor.extended_resolution_errors`
+
 If set, the recursor will add an EDNS Extended Error (:rfc:`8914`) to responses when resolution failed, like DNSSEC validation errors, explaining the reason it failed. This setting is not needed to allow setting custom error codes from Lua or from a RPZ hit.
 
 .. _setting-forward-zones:
 
 ``forward-zones``
------------------
--  'zonename=IP' pairs, comma separated
+~~~~~~~~~~~~~~~~~
+
+-  Comma separated list of 'zonename=IP' pairs
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.forward_zones`
 
 Queries for zones listed here will be forwarded to the IP address listed. i.e.
 
@@ -882,75 +1040,83 @@ If an ``NS`` record set for a subzone of the forwarded zone is learned, that rec
 This allows e.g. a forward to a local authoritative server holding a copy of the root zone, delegations received from that server will work.
 
 **IMPORTANT**: When using DNSSEC validation (which is default), forwards to non-delegated (e.g. internal) zones that have a DNSSEC signed parent zone will validate as Bogus.
-To prevent this, add a Negative Trust Anchor (NTA) for this zone in the `lua-config-file`_ with ``addNTA("your.zone", "A comment")``.
-If this forwarded zone is signed, instead of adding NTA, add the DS record to the `lua-config-file`_.
+To prevent this, add a Negative Trust Anchor (NTA) for this zone in the :ref:`setting-lua-config-file` with ``addNTA('your.zone', 'A comment')``.
+If this forwarded zone is signed, instead of adding NTA, add the DS record to the :ref:`setting-lua-config-file`.
 See the :doc:`dnssec` information.
 
 .. _setting-forward-zones-file:
 
 ``forward-zones-file``
-----------------------
--  Path
+~~~~~~~~~~~~~~~~~~~~~~
+.. versionchanged:: 4.0.0
 
-Same as `forward-zones`_, parsed from a file. Only 1 zone is allowed per line, specified as follows:
+  (Old style settings only) Comments are allowed, everything behind ``#`` is ignored.
+.. versionchanged:: 4.6.0
 
-.. code-block:: none
+  (Old style settings only) Zones prefixed with a ``^`` are added to the :ref:`setting-allow-notify-for` list. Both prefix characters can be used if desired, in any order.
 
-    example.org=203.0.113.210, 192.0.2.4:5300
+-  String
+-  Default: (empty)
 
-Zones prefixed with a '+' are treated as with
-`forward-zones-recurse`_.  Default behaviour without '+' is as with
-`forward-zones`_.
+- YAML setting: :ref:`setting-yaml-recursor.forward_zones_file`
 
-.. versionchanged:: 4.0.0
+Same as :ref:`setting-forward-zones`, parsed from a file. Only 1 zone is allowed per line, specified as follows:
 
-  Comments are allowed, everything behind '#' is ignored.
+.. code-block:: none
 
-The DNSSEC notes from `forward-zones`_ apply here as well.
+    example.org=203.0.113.210, 192.0.2.4:5300
 
-.. versionchanged:: 4.6.0
+Zones prefixed with a ``+`` are treated as with
+:ref:`setting-forward-zones-recurse`.  Default behaviour without ``+`` is as with
+:ref:`setting-forward-zones`.
 
-Zones prefixed with a '^' are added to the `allow-notify-for`_
-list. Both prefix characters can be used if desired, in any order.
+The DNSSEC notes from :ref:`setting-forward-zones` apply here as well.
 
 .. _setting-forward-zones-recurse:
 
 ``forward-zones-recurse``
--------------------------
--  'zonename=IP' pairs, comma separated
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+-  Comma separated list of 'zonename=IP' pairs
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.forward_zones_recurse`
 
-Like regular `forward-zones`_, but forwarded queries have the ``recursion desired (RD)`` bit set to ``1``, meaning that this setting is intended to forward queries to other recursive servers.
+Like regular :ref:`setting-forward-zones`, but forwarded queries have the ``recursion desired (RD)`` bit set to ``1``, meaning that this setting is intended to forward queries to other recursive servers.
 In contrast to regular forwarding, the rule that delegations of the forwarded subzones are respected is not active.
 This is because we rely on the forwarder to resolve the query fully.
 
-See `forward-zones`_ for additional options (such as supplying multiple recursive servers) and an important note about DNSSEC.
+See :ref:`setting-forward-zones` for additional options (such as supplying multiple recursive servers) and an important note about DNSSEC.
 
 .. _setting-gettag-needs-edns-options:
 
 ``gettag-needs-edns-options``
------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-incoming.gettag_needs_edns_options`
+
 If set, EDNS options in incoming queries are extracted and passed to the :func:`gettag` hook in the ``ednsoptions`` table.
 
 .. _setting-hint-file:
 
 ``hint-file``
--------------
--  Path
--  Default: empty
-
+~~~~~~~~~~~~~
 .. versionchanged:: 4.6.2
 
   Introduced the value ``no`` to disable root-hints processing.
-
 .. versionchanged:: 4.9.0
 
   Introduced the value ``no-refresh`` to disable both root-hints processing and periodic refresh of the cached root `NS` records.
 
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.hint_file`
+
 If set, the root-hints are read from this file. If empty, the default built-in root hints are used.
 
 In some special cases, processing the root hints is not needed, for example when forwarding all queries to another recursor.
@@ -960,12 +1126,12 @@ See :ref:`handling-of-root-hints` for more information on root hints handling.
 .. _setting-ignore-unknown-settings:
 
 ``ignore-unknown-settings``
----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. versionadded:: 4.6.0
+-  Comma separated list of strings
+-  Default: (empty)
 
--  Setting names, separated by commas
--  Default: empty
+- YAML setting: :ref:`setting-yaml-recursor.ignore_unknown_settings`
 
 Names of settings to be ignored while parsing configuration files, if the setting
 name is unknown to PowerDNS.
@@ -975,26 +1141,36 @@ Useful during upgrade testing.
 .. _setting-include-dir:
 
 ``include-dir``
----------------
--  Path
+~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.include_dir`
 
 Directory to scan for additional config files. All files that end with .conf are loaded in order using ``POSIX`` as locale.
 
 .. _setting-latency-statistic-size:
 
 ``latency-statistic-size``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 10000
 
+- YAML setting: :ref:`setting-yaml-recursor.latency_statistic_size`
+
 Indication of how many queries will be averaged to get the average latency reported by the 'qa-latency' metric.
 
 .. _setting-local-address:
 
 ``local-address``
------------------
--  IPv4/IPv6 Addresses, with optional port numbers, separated by commas or whitespace
--  Default: ``127.0.0.1``
+~~~~~~~~~~~~~~~~~
+
+-  Comma separated list or IPs of IP:port combinations
+-  Default: 127.0.0.1
+
+- YAML setting: :ref:`setting-yaml-incoming.listen`
 
 Local IP addresses to which we bind. Each address specified can
 include a port number; if no port is included then the
@@ -1013,75 +1189,91 @@ Examples::
 .. _setting-local-port:
 
 ``local-port``
---------------
+~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 53
 
+- YAML setting: :ref:`setting-yaml-incoming.port`
+
 Local port to bind to.
-If an address in `local-address`_ does not have an explicit port, this port is used.
+If an address in :ref:`setting-local-address` does not have an explicit port, this port is used.
 
 .. _setting-log-timestamp:
 
 ``log-timestamp``
------------------
+~~~~~~~~~~~~~~~~~
 
-.. versionadded:: 4.1.0
+-  Boolean
+-  Default: yes
 
-- Bool
-- Default: yes
+- YAML setting: :ref:`setting-yaml-logging.timestamp`
 
-When printing log lines to stdout, prefix them with timestamps.
-Disable this if the process supervisor timestamps these lines already.
 
-.. note::
-  The systemd unit file supplied with the source code already disables timestamp printing
 
 .. _setting-non-local-bind:
 
 ``non-local-bind``
-------------------
+~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
-Bind to addresses even if one or more of the `local-address`_'s do not exist on this server.
+- YAML setting: :ref:`setting-yaml-incoming.non_local_bind`
+
+Bind to addresses even if one or more of the :ref:`setting-local-address`'s do not exist on this server.
 Setting this option will enable the needed socket options to allow binding to non-local addresses.
 This feature is intended to facilitate ip-failover setups, but it may also mask configuration issues and for this reason it is disabled by default.
 
 .. _setting-loglevel:
 
 ``loglevel``
-------------
--  Integer between 0 and 9
+~~~~~~~~~~~~
+
+-  Integer
 -  Default: 6
 
+- YAML setting: :ref:`setting-yaml-logging.loglevel`
+
 Amount of logging. The higher the number, the more lines logged.
-Corresponds to "syslog" level values (e.g. 0 = emergency, 1 = alert, 2 = critical, 3 = error, 4 = warning, 5 = notice, 6 = info, 7 = debug).
+Corresponds to 'syslog' level values (e.g. 0 = emergency, 1 = alert, 2 = critical, 3 = error, 4 = warning, 5 = notice, 6 = info, 7 = debug).
 Each level includes itself plus the lower levels before it.
 Not recommended to set this below 3.
 
 .. _setting-log-common-errors:
 
 ``log-common-errors``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-logging.common_errors`
+
 Some DNS errors occur rather frequently and are no cause for alarm.
 
+.. _setting-log-rpz-changes:
+
 ``log-rpz-changes``
--------------------
+~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-logging.rpz_changes`
+
 Log additions and removals to RPZ zones at Info (6) level instead of Debug (7).
 
 .. _setting-logging-facility:
 
 ``logging-facility``
---------------------
--  Integer
+~~~~~~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-logging.facility`
 
 If set to a digit, logging is performed under this LOCAL facility.
 See :ref:`logging`.
@@ -1090,10 +1282,13 @@ Do not pass names like 'local0'!
 .. _setting-lowercase-outgoing:
 
 ``lowercase-outgoing``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-outgoing.lowercase`
+
 Set to true to lowercase the outgoing queries.
 When set to 'no' (the default) a query from a client using mixed case in the DNS labels (such as a user entering mixed-case names or `draft-vixie-dnsext-dns0x20-00 <http://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00>`_), PowerDNS preserves the case of the query.
 Broken authoritative servers might give a wrong or broken answer on this encoding.
@@ -1102,8 +1297,12 @@ Setting ``lowercase-outgoing`` to 'yes' makes the PowerDNS Recursor lowercase al
 .. _setting-lua-config-file:
 
 ``lua-config-file``
--------------------
--  Filename
+~~~~~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.lua_config_file`
 
 If set, and Lua support is compiled in, this will load an additional configuration file for newer features and more complicated setups.
 See :doc:`lua-config/index` for the options that can be set in this file.
@@ -1111,21 +1310,25 @@ See :doc:`lua-config/index` for the options that can be set in this file.
 .. _setting-lua-dns-script:
 
 ``lua-dns-script``
-------------------
--  Path
--  Default: unset
+~~~~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.lua_dns_script`
 
 Path to a lua file to manipulate the Recursor's answers. See :doc:`lua-scripting/index` for more information.
 
-.. _setting-maintenance-interval:
+.. _setting-lua-maintenance-interval:
 
 ``lua-maintenance-interval``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  Integer
 -  Default: 1
 
+- YAML setting: :ref:`setting-yaml-recursor.lua_maintenance_interval`
 
 The interval between calls to the Lua user defined `maintenance()` function in seconds.
 See :ref:`hooks-maintenance-callback`
@@ -1133,11 +1336,13 @@ See :ref:`hooks-maintenance-callback`
 .. _setting-max-busy-dot-probes:
 
 ``max-busy-dot-probes``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.7.0
 
-- Integer
-- Default: 0
+-  Integer
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-outgoing.max_busy_dot_probes`
 
 Limit the maximum number of simultaneous DoT probes the Recursor will schedule.
 The default value 0 means no DoT probes are scheduled.
@@ -1150,7 +1355,6 @@ If the maximum number of pending probes is reached, no probes will be scheduled,
 If the result of a probe is not yet available, the Recursor will contact the authoritative server in the regular way, unless an authoritative server is configured to be contacted over DoT always using :ref:`setting-dot-to-auth-names`.
 In that case no probe will be scheduled.
 
-
 .. note::
   DoT probing is an experimental feature.
   Please test thoroughly to determine if it is suitable in your specific production environment before enabling.
@@ -1158,21 +1362,26 @@ In that case no probe will be scheduled.
 .. _setting-max-cache-bogus-ttl:
 
 ``max-cache-bogus-ttl``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  Integer
 -  Default: 3600
 
+- YAML setting: :ref:`setting-yaml-recordcache.max_cache_bogus_ttl`
+
 Maximum number of seconds to cache an item in the DNS cache (negative or positive) if its DNSSEC validation failed, no matter what the original TTL specified, to reduce the impact of a broken domain.
 
 .. _setting-max-cache-entries:
 
 ``max-cache-entries``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 1000000
 
+- YAML setting: :ref:`setting-yaml-recordcache.max_entries`
+
 Maximum number of DNS record cache entries, shared by all threads since 4.4.0.
 Each entry associates a name and type with a record set.
 The size of the negative cache is 10% of this number.
@@ -1180,28 +1389,31 @@ The size of the negative cache is 10% of this number.
 .. _setting-max-cache-ttl:
 
 ``max-cache-ttl``
------------------
+~~~~~~~~~~~~~~~~~
+.. versionchanged:: 4.1.0
+
+  The minimum value of this setting is 15. i.e. setting this to lower than 15 will make this value 15.
+
 -  Integer
 -  Default: 86400
 
+- YAML setting: :ref:`setting-yaml-recordcache.max_ttl`
+
 Maximum number of seconds to cache an item in the DNS cache, no matter what the original TTL specified.
 This value also controls the refresh period of cached root data.
 See :ref:`handling-of-root-hints` for more information on this.
 
-.. versionchanged:: 4.1.0
-
-    The minimum value of this setting is 15. i.e. setting this to lower than 15 will make this value 15.
-
 .. _setting-max-concurrent-requests-per-tcp-connection:
 
 ``max-concurrent-requests-per-tcp-connection``
-----------------------------------------------
-
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.3.0
 
 -  Integer
 -  Default: 10
 
+- YAML setting: :ref:`setting-yaml-incoming.max_concurrent_requests_per_tcp_connection`
+
 Maximum number of incoming requests handled concurrently per tcp
 connection. This number must be larger than 0 and smaller than 65536
 and also smaller than `max-mthreads`.
@@ -1209,26 +1421,28 @@ and also smaller than `max-mthreads`.
 .. _setting-max-include-depth:
 
 ``max-include-depth``
-----------------------
-
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
 -  Integer
 -  Default: 20
 
+- YAML setting: :ref:`setting-yaml-recursor.max_include_depth`
+
 Maximum number of nested ``$INCLUDE`` directives while processing a zone file.
 Zero mean no ``$INCLUDE`` directives will be accepted.
 
 .. _setting-max-generate-steps:
 
 ``max-generate-steps``
-----------------------
-
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.3.0
 
 -  Integer
 -  Default: 0
 
+- YAML setting: :ref:`setting-yaml-recursor.max_generate_steps`
+
 Maximum number of steps for a '$GENERATE' directive when parsing a
 zone file. This is a protection measure to prevent consuming a lot of
 CPU and memory when untrusted zones are loaded. Default to 0 which
@@ -1237,28 +1451,37 @@ means unlimited.
 .. _setting-max-mthreads:
 
 ``max-mthreads``
-----------------
+~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 2048
 
+- YAML setting: :ref:`setting-yaml-recursor.max_mthreads`
+
 Maximum number of simultaneous MTasker threads.
 
 .. _setting-max-packetcache-entries:
 
 ``max-packetcache-entries``
----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 500000
 
+- YAML setting: :ref:`setting-yaml-packetcache.max_entries`
+
 Maximum number of Packet Cache entries. Sharded and shared by all threads since 4.9.0.
 
 .. _setting-max-qperq:
 
 ``max-qperq``
--------------
+~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 60
 
+- YAML setting: :ref:`setting-yaml-recursor.max_qperq`
+
 The maximum number of outgoing queries that will be sent out during the resolution of a single client query.
 This is used to limit endlessly chasing CNAME redirections.
 If qname-minimization is enabled, the number will be forced to be 100
@@ -1267,7 +1490,7 @@ at a minimum to allow for the extra queries qname-minimization generates when th
 .. _setting-max-ns-address-qperq:
 
 ``max-ns-address-qperq``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.16
 .. versionadded:: 4.2.2
 .. versionadded:: 4.3.1
@@ -1275,19 +1498,21 @@ at a minimum to allow for the extra queries qname-minimization generates when th
 -  Integer
 -  Default: 10
 
+- YAML setting: :ref:`setting-yaml-recursor.max_ns_address_qperq`
+
 The maximum number of outgoing queries with empty replies for
 resolving nameserver names to addresses we allow during the resolution
 of a single client query. If IPv6 is enabled, an A and a AAAA query
 for a name counts as 1. If a zone publishes more than this number of
 NS records, the limit is further reduced for that zone by lowering
 it by the number of NS records found above the
-`max-ns-address-qperq`_ value. The limit wil not be reduced to a
+:ref:`setting-max-ns-address-qperq` value. The limit wil not be reduced to a
 number lower than 5.
 
 .. _setting-max-ns-per-resolve:
 
 ``max-ns-per-resolve``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.8.0
 .. versionadded:: 4.7.3
 .. versionadded:: 4.6.4
@@ -1296,17 +1521,22 @@ number lower than 5.
 -  Integer
 -  Default: 13
 
+- YAML setting: :ref:`setting-yaml-recursor.max_ns_per_resolve`
+
 The maximum number of NS records that will be considered to select a nameserver to contact to resolve a name.
-If a zone has more than `max-ns-per-resolve`_ NS records, a random sample of this size will be used.
-If `max-ns-per-resolve`_ is zero, no limit applies.
+If a zone has more than :ref:`setting-max-ns-per-resolve` NS records, a random sample of this size will be used.
+If :ref:`setting-max-ns-per-resolve` is zero, no limit applies.
 
 .. _setting-max-negative-ttl:
 
 ``max-negative-ttl``
---------------------
+~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 3600
 
+- YAML setting: :ref:`setting-yaml-recordcache.max_negative_ttl`
+
 A query for which there is authoritatively no answer is cached to quickly deny a record's existence later on, without putting a heavy load on the remote server.
 In practice, caches can become saturated with hundreds of thousands of hosts which are tried only once.
 This setting, which defaults to 3600 seconds, puts a maximum on the amount of time negative entries are cached.
@@ -1314,71 +1544,87 @@ This setting, which defaults to 3600 seconds, puts a maximum on the amount of ti
 .. _setting-max-recursion-depth:
 
 ``max-recursion-depth``
------------------------
--  Integer
--  Default: 16
-
-Total maximum number of internal recursion calls the server may use to answer a single query.
-0 means unlimited.
-The value of `stack-size`_ should be increased together with this one to prevent the stack from overflowing.
-If `qname-minimization`_ is enabled, the fallback code in case of a failing resolve is allowed an additional `max-recursion-depth/2`.
+~~~~~~~~~~~~~~~~~~~~~~~
+.. versionchanged:: 4.1.0
 
+  Before 4.1.0, this settings was unlimited.
+.. versionchanged:: 4.9.0
 
-.. versionchanged:: 4.1.0
+  Before 4.9.0 this setting's default was 40 and the limit on ``CNAME`` chains (fixed at 16) acted as a bound on he recursion depth.
 
-    Before 4.1.0, this settings was unlimited.
+-  Integer
+-  Default: 16
 
-.. versionchanged:: 4.9.0
+- YAML setting: :ref:`setting-yaml-recursor.max_recursion_depth`
 
-   Before 4.9.0 this setting's default was 40 and the limit on ``CNAME`` chains (fixed at 16) acted as a bound on he recursion depth.
+Total maximum number of internal recursion calls the server may use to answer a single query.
+0 means unlimited.
+The value of :ref:`setting-stack-size` should be increased together with this one to prevent the stack from overflowing.
+If :ref:`setting-qname-minimization` is enabled, the fallback code in case of a failing resolve is allowed an additional `max-recursion-depth/2`.
 
 .. _setting-max-tcp-clients:
 
 ``max-tcp-clients``
--------------------
+~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 128
 
+- YAML setting: :ref:`setting-yaml-incoming.max_tcp_clients`
+
 Maximum number of simultaneous incoming TCP connections allowed.
 
 .. _setting-max-tcp-per-client:
 
 ``max-tcp-per-client``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
--  Default: 0 (unlimited)
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-incoming.max_tcp_per_client`
 
 Maximum number of simultaneous incoming TCP connections allowed per client (remote IP address).
+ 0 means unlimited.
 
 .. _setting-max-tcp-queries-per-connection:
 
 ``max-tcp-queries-per-connection``
-----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Integer
--  Default: 0 (unlimited)
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-incoming.max_tcp_queries_per_connection`
 
 Maximum number of DNS queries in a TCP connection.
+0 means unlimited.
 
 .. _setting-max-total-msec:
 
 ``max-total-msec``
-------------------
+~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 7000
 
+- YAML setting: :ref:`setting-yaml-recursor.max_total_msec`
+
 Total maximum number of milliseconds of wallclock time the server may use to answer a single query.
+0 means unlimited.
 
 .. _setting-max-udp-queries-per-round:
 
 ``max-udp-queries-per-round``
-----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.4
 
 -  Integer
 -  Default: 10000
 
+- YAML setting: :ref:`setting-yaml-incoming.max_udp_queries_per_round`
+
 Under heavy load the recursor might be busy processing incoming UDP queries for a long while before there is no more of these, and might therefore
 neglect scheduling new ``mthreads``, handling responses from authoritative servers or responding to :doc:`rec_control <manpages/rec_control.1>`
 requests.
@@ -1388,13 +1634,16 @@ returning back to normal processing and handling other events.
 .. _setting-minimum-ttl-override:
 
 ``minimum-ttl-override``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.5.0
+
   Old versions used default 0.
 
 -  Integer
 -  Default: 1
 
+- YAML setting: :ref:`setting-yaml-recursor.minimum_ttl_override`
+
 This setting artificially raises all TTLs to be at least this long.
 Setting this to a value greater than 1 technically is an RFC violation, but might improve performance a lot.
 Using a value of 0 impacts performance of TTL 0 records greatly, since it forces the recursor to contact
@@ -1404,11 +1653,13 @@ Can be set at runtime using ``rec_control set-minimum-ttl 3600``.
 .. _setting-new-domain-tracking:
 
 ``new-domain-tracking``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Boolean
-- Default: no (disabled)
+-  Boolean
+-  Default: no
+
+- YAML setting: :ref:`setting-yaml-nod.tracking`
 
 Whether to track newly observed domains, i.e. never seen before. This
 is a probabilistic algorithm, using a stable bloom filter to store
@@ -1423,29 +1674,33 @@ status will appear as a flag in Response messages.
 .. _setting-new-domain-log:
 
 ``new-domain-log``
-------------------
+~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Boolean
-- Default: yes (enabled)
+-  Boolean
+-  Default: yes
+
+- YAML setting: :ref:`setting-yaml-nod.log`
 
 If a newly observed domain is detected, log that domain in the
 recursor log file. The log line looks something like::
 
 Jul 18 11:31:25 Newly observed domain nod=sdfoijdfio.com
+ Jul 18 11:31:25 Newly observed domain nod=sdfoijdfio.com
 
 .. _setting-new-domain-lookup:
 
 ``new-domain-lookup``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Domain Name
-- Example: nod.powerdns.com
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-nod.lookup`
 
 If a domain is specified, then each time a newly observed domain is
-detected, the recursor will perform an A record lookup of "<newly
-observed domain>.<lookup domain>". For example if 'new-domain-lookup'
+detected, the recursor will perform an A record lookup of '<newly
+observed domain>.<lookup domain>'. For example if 'new-domain-lookup'
 is configured as 'nod.powerdns.com', and a new domain 'xyz123.tv' is
 detected, then an A record lookup will be made for
 'xyz123.tv.nod.powerdns.com'. This feature gives a way to share the
@@ -1455,11 +1710,13 @@ result of the DNS lookup will be ignored by the recursor.
 .. _setting-new-domain-db-size:
 
 ``new-domain-db-size``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Integer
-- Example: 67108864
+-  Integer
+-  Default: 67108864
+
+- YAML setting: :ref:`setting-yaml-nod.db_size`
 
 The default size of the stable bloom filter used to store previously
 observed domains is 67108864. To change the number of cells, use this
@@ -1471,10 +1728,13 @@ have no effect unless you remove the existing files.
 .. _setting-new-domain-history-dir:
 
 ``new-domain-history-dir``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Path
+-  String
+-  Default: /usr/local/var/lib/pdns-recursor/nod
+
+- YAML setting: :ref:`setting-yaml-nod.history_dir`
 
 This setting controls which directory is used to store the on-disk
 cache of previously observed domains.
@@ -1493,19 +1753,29 @@ from this directory.
 .. _setting-new-domain-whitelist:
 
 ``new-domain-whitelist``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 .. deprecated:: 4.5.0
+
   Use :ref:`setting-new-domain-ignore-list`.
 
+-  String
+-  Default: (empty)
+
+- YAML setting does not exist
+
+
+
 .. _setting-new-domain-ignore-list:
 
 ``new-domain-ignore-list``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
-- List of Domain Names, comma separated
-- Example: xyz.com, abc.com
+-  Comma separated list of strings
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-nod.ignore_list`
 
 This setting is a list of all domains (and implicitly all subdomains)
 that will never be considered a new domain. For example, if the domain
@@ -1517,11 +1787,13 @@ feature.
 .. _setting-new-domain-pb-tag:
 
 ``new-domain-pb-tag``
----------------------
+~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- String
-- Default: pnds-nod
+-  String
+-  Default: pdns-nod
+
+- YAML setting: :ref:`setting-yaml-nod.pb_tag`
 
 If protobuf is configured, then this tag will be added to all protobuf response messages when
 a new domain is observed.
@@ -1529,44 +1801,54 @@ a new domain is observed.
 .. _setting-network-timeout:
 
 ``network-timeout``
--------------------
+~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 1500
 
+- YAML setting: :ref:`setting-yaml-outgoing.network_timeout`
+
 Number of milliseconds to wait for a remote authoritative server to respond.
 
 .. _setting-non-resolving-ns-max-fails:
 
 ``non-resolving-ns-max-fails``
-------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
-- Integer
-- Default: 5
+-  Integer
+-  Default: 5
+
+- YAML setting: :ref:`setting-yaml-recursor.non_resolving_ns_max_fails`
 
 Number of failed address resolves of a nameserver name to start throttling it, 0 is disabled.
 Nameservers matching :ref:`setting-dont-throttle-names` will not be throttled.
 
-
 .. _setting-non-resolving-ns-throttle-time:
 
-``non-resolving-ns-max-throttle-time``
---------------------------------------
+``non-resolving-ns-throttle-time``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
-- Integer
-- Default: 60
+-  Integer
+-  Default: 60
+
+- YAML setting: :ref:`setting-yaml-recursor.non_resolving_ns_throttle_time`
 
 Number of seconds to throttle a nameserver with a name failing to resolve.
 
 .. _setting-nothing-below-nxdomain:
 
 ``nothing-below-nxdomain``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.3.0
 
-- One of ``no``, ``dnssec``, ``yes``, String
-- Default: ``dnssec``
+-  String
+-  Default: dnssec
+
+- YAML setting: :ref:`setting-yaml-recursor.nothing_below_nxdomain`
+
+- One of ``no``, ``dnssec``, ``yes``.
 
 The type of :rfc:`8020` handling using cached NXDOMAIN responses.
 This RFC specifies that NXDOMAIN means that the DNS tree under the denied name MUST be empty.
@@ -1590,200 +1872,210 @@ without consulting authoritative servers.
 .. _setting-nsec3-max-iterations:
 
 ``nsec3-max-iterations``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
+.. versionchanged:: 4.5.2
+
+  Default is now 150, was 2500 before.
 
 -  Integer
 -  Default: 150
 
+- YAML setting: :ref:`setting-yaml-dnssec.nsec3_max_iterations`
+
 Maximum number of iterations allowed for an NSEC3 record.
 If an answer containing an NSEC3 record with more iterations is received, its DNSSEC validation status is treated as Insecure.
 
-.. versionchanged:: 4.5.2
-
-   Default is now 150, was 2500 before.
-
 .. _setting-packetcache-ttl:
 
 ``packetcache-ttl``
--------------------
+~~~~~~~~~~~~~~~~~~~
+.. versionchanged:: 4.9.0
+
+  The default was changed from 3600 (1 hour) to 86400 (24 hours).
+
 -  Integer
 -  Default: 86400
 
-Maximum number of seconds to cache an item in the packet cache, no matter what the original TTL specified.
+- YAML setting: :ref:`setting-yaml-packetcache.ttl`
 
-.. versionchanged:: 4.9.0
-
-   The default was changed from 3600 (1 hour) to 86400 (24 hours).
+Maximum number of seconds to cache an item in the packet cache, no matter what the original TTL specified.
 
 .. _setting-packetcache-negative-ttl:
 
 ``packetcache-negative-ttl``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.9.0
 
 -  Integer
 -  Default: 60
 
+- YAML setting: :ref:`setting-yaml-packetcache.negative_ttl`
+
 Maximum number of seconds to cache an ``NxDomain`` or ``NoData`` answer in the packetcache.
-This setting's maximum is capped to `packetcache-ttl`_.
+This setting's maximum is capped to :ref:`setting-packetcache-ttl`.
 i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-negative-ttl`` at the default will lower ``packetcache-negative-ttl`` to ``15``.
 
 .. _setting-packetcache-servfail-ttl:
 
 ``packetcache-servfail-ttl``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+'versionchanged': ('4.0.0', "This setting's maximum is capped to :ref:`setting-packetcache-ttl`.
+    i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-servfail-ttl`` at the default will lower ``packetcache-servfail-ttl`` to ``15``.")
+
 -  Integer
 -  Default: 60
 
+- YAML setting: :ref:`setting-yaml-packetcache.servfail_ttl`
+
 Maximum number of seconds to cache an answer indicating a failure to resolve in the packet cache.
 Before version 4.6.0 only ``ServFail`` answers were considered as such. Starting with 4.6.0, all responses with a code other than ``NoError`` and ``NXDomain``, or without records in the answer and authority sections, are considered as a failure to resolve.
 Since 4.9.0, negative answers are handled separately from resolving failures.
 
-.. versionchanged:: 4.0.0
-
-    This setting's maximum is capped to `packetcache-ttl`_.
-    i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-servfail-ttl`` at the default will lower ``packetcache-servfail-ttl`` to ``15``.
-
-
 .. _setting-packetcache-shards:
 
 ``packetcache-shards``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.9.0
 
 -  Integer
 -  Default: 1024
 
+- YAML setting: :ref:`setting-yaml-packetcache.shards`
+
 Sets the number of shards in the packet cache. If you have high contention as reported by ``packetcache-contented/packetcache-acquired``,
 you can try to enlarge this value or run with fewer threads.
 
 .. _setting-pdns-distributes-queries:
 
 ``pdns-distributes-queries``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. versionchanged:: 4.9.0
+
+  Default changed to ``no``, previously it was ``yes``.
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-incoming.pdns_distributes_queries`
+
 If set, PowerDNS will use distinct threads to listen to client sockets and distribute that work to worker-threads using a hash of the query.
 This feature should maximize the cache hit ratio on versions before 4.9.0.
-To use more than one thread set `distributor-threads`_ in version 4.2.0 or newer.
-Enabling should improve performance on systems where `reuseport`_ does not have the effect of
+To use more than one thread set :ref:`setting-distributor-threads` in version 4.2.0 or newer.
+Enabling should improve performance on systems where :ref:`setting-reuseport` does not have the effect of
 balancing the queries evenly over multiple worker threads.
 
-.. versionchanged:: 4.9.0
-
-   Default changed to ``no``, previously it was ``yes``.
-
 .. _setting-protobuf-use-kernel-timestamp:
 
 ``protobuf-use-kernel-timestamp``
----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Boolean
-- Default: false
+-  Boolean
+-  Default: no
+
+- YAML setting: :ref:`setting-yaml-logging.protobuf_use_kernel_timestamp`
 
 Whether to compute the latency of responses in protobuf messages using the timestamp set by the kernel when the query packet was received (when available), instead of computing it based on the moment we start processing the query.
 
 .. _setting-proxy-protocol-from:
 
 ``proxy-protocol-from``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.4.0
 
--  IP addresses or netmasks, separated by commas, negation supported
--  Default: empty
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-incoming.proxy_protocol_from`
 
 Ranges that are required to send a Proxy Protocol version 2 header in front of UDP and TCP queries, to pass the original source and destination addresses and ports to the recursor, as well as custom values.
 Queries that are not prefixed with such a header will not be accepted from clients in these ranges. Queries prefixed by headers from clients that are not listed in these ranges will be dropped.
 
-Note that once a Proxy Protocol header has been received, the source address from the proxy header instead of the address of the proxy will be checked against the `allow-from`_ ACL.
+Note that once a Proxy Protocol header has been received, the source address from the proxy header instead of the address of the proxy will be checked against the :ref:`setting-allow-from` ACL.
 
 The dnsdist docs have `more information about the PROXY protocol <https://dnsdist.org/advanced/passing-source-address.html#proxy-protocol>`_.
 
 .. _setting-proxy-protocol-maximum-size:
 
 ``proxy-protocol-maximum-size``
--------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.4.0
 
 -  Integer
 -  Default: 512
 
+- YAML setting: :ref:`setting-yaml-incoming.proxy_protocol_maximum_size`
+
 The maximum size, in bytes, of a Proxy Protocol payload (header, addresses and ports, and TLV values). Queries with a larger payload will be dropped.
 
 .. _setting-public-suffix-list-file:
 
 ``public-suffix-list-file``
----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Path
-- Default: unset
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.public_suffix_list_file`
 
 Path to the Public Suffix List file, if any. If set, PowerDNS will try to load the Public Suffix List from this file instead of using the built-in list. The PSL is used to group the queries by relevant domain names when displaying the top queries.
 
 .. _setting-qname-minimization:
 
 ``qname-minimization``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.3.0
 
 -  Boolean
 -  Default: yes
 
+- YAML setting: :ref:`setting-yaml-recursor.qname_minimization`
+
 Enable Query Name Minimization. This implements a relaxed form of Query Name Mimimization as
 described in :rfc:`7816`.
 
 .. _setting-query-local-address:
 
 ``query-local-address``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.4.0
+
   IPv6 addresses can be set with this option as well.
 
--  IP addresses, comma separated
+-  Comma separated list of IP addresses or subnets, negation supported
 -  Default: 0.0.0.0
 
+- YAML setting: :ref:`setting-yaml-outgoing.source_address`
+
 Send out local queries from this address, or addresses. By adding multiple
 addresses, increased spoofing resilience is achieved. When no address of a certain
 address family is configured, there are *no* queries sent with that address family.
 In the default configuration this means that IPv6 is not used for outgoing queries.
 
-.. _setting-query-local-address6:
-
-``query-local-address6``
-------------------------
-.. deprecated:: 4.4.0
-  Use :ref:`setting-query-local-address` for IPv4 and IPv6.
-
-.. deprecated:: 4.5.0
-  Removed, use :ref:`setting-query-local-address`.
-
--  IPv6 addresses, comma separated
--  Default: unset
-
-Send out local IPv6 queries from this address or addresses.
-Disabled by default, which also disables outgoing IPv6 support.
-
 .. _setting-quiet:
 
 ``quiet``
----------
+~~~~~~~~~
+
 -  Boolean
 -  Default: yes
 
+- YAML setting: :ref:`setting-yaml-logging.quiet`
+
 Don't log queries.
 
 .. _setting-record-cache-locked-ttl-perc:
 
 ``record-cache-locked-ttl-perc``
---------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.8.0
 
-- Integer
-- Default: 0
+-  Integer
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-recordcache.locked_ttl_perc`
 
 Replace record sets in the record cache only after this percentage of the original TTL has passed.
 The PowerDNS Recursor already has several mechanisms to protect against spoofing attempts.
@@ -1803,12 +2095,14 @@ There are a few cases where records will be replaced anyway:
 .. _setting-record-cache-shards:
 
 ``record-cache-shards``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.4.0
 
 -  Integer
 -  Default: 1024
 
+- YAML setting: :ref:`setting-yaml-recordcache.shards`
+
 Sets the number of shards in the record cache. If you have high
 contention as reported by
 ``record-cache-contented/record-cache-acquired``, you can try to
@@ -1817,13 +2111,15 @@ enlarge this value or run with fewer threads.
 .. _setting-refresh-on-ttl-perc:
 
 ``refresh-on-ttl-perc``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Integer
 -  Default: 0
 
-Sets the "refresh almost expired" percentage of the record cache. Whenever a record is fetched from the packet or record cache
+- YAML setting: :ref:`setting-yaml-recordcache.refresh_on_ttl_perc`
+
+Sets the 'refresh almost expired' percentage of the record cache. Whenever a record is fetched from the packet or record cache
 and only ``refresh-on-ttl-perc`` percent or less of its original TTL is left, a task is queued to refetch the name/type combination to
 update the record cache. In most cases this causes future queries to always see a non-expired record cache entry.
 A typical value is 10. If the value is zero, this functionality is disabled.
@@ -1831,23 +2127,29 @@ A typical value is 10. If the value is zero, this functionality is disabled.
 .. _setting-reuseport:
 
 ``reuseport``
--------------
+~~~~~~~~~~~~~
+.. versionchanged:: 4.9.0
+
+  The default is changed to ``yes``, previously it was ``no``. If ``SO_REUSEPORT`` support is not available, the setting defaults to ``no``.
+
 -  Boolean
 -  Default: yes
 
-If ``SO_REUSEPORT`` support is available, allows multiple threads and processes to open listening sockets for the same port.
+- YAML setting: :ref:`setting-yaml-incoming.reuseport`
 
-Since 4.1.0, when `pdns-distributes-queries`_ is disabled and `reuseport`_ is enabled, every worker-thread will open a separate listening socket to let the kernel distribute the incoming queries instead of running a distributor thread (which could otherwise be a bottleneck) and avoiding thundering herd issues, thus leading to much higher performance on multi-core boxes.
-
-.. versionchanged:: 4.9.0
+If ``SO_REUSEPORT`` support is available, allows multiple threads and processes to open listening sockets for the same port.
 
-   The default is changed to ``yes``, previously it was ``no``.
-   If ``SO_REUSEPORT`` support is not available, the setting defaults to ``no``.
+Since 4.1.0, when :ref:`setting-pdns-distributes-queries` is disabled and :ref:`setting-reuseport` is enabled, every worker-thread will open a separate listening socket to let the kernel distribute the incoming queries instead of running a distributor thread (which could otherwise be a bottleneck) and avoiding thundering herd issues, thus leading to much higher performance on multi-core boxes.
 
 .. _setting-rng:
 
 ``rng``
--------
+~~~~~~~
+
+-  String
+-  Default: auto
+
+- YAML setting: :ref:`setting-yaml-recursor.rng`
 
 - String
 - Default: auto
@@ -1861,31 +2163,32 @@ Specify which random number generator to use. Permissible choices are
  - urandom - Use ``/dev/urandom``
  - kiss - Use simple settable deterministic RNG. **FOR TESTING PURPOSES ONLY!**
 
-.. note::
-  Not all choices are available on all systems.
-
 .. _setting-root-nx-trust:
 
 ``root-nx-trust``
------------------
+~~~~~~~~~~~~~~~~~
+.. versionchanged:: 4.0.0
+
+  Default is ``yes`` now, was ``no`` before 4.0.0
+
 -  Boolean
 -  Default: yes
 
+- YAML setting: :ref:`setting-yaml-recursor.root_nx_trust`
+
 If set, an NXDOMAIN from the root-servers will serve as a blanket NXDOMAIN for the entire TLD the query belonged to.
 The effect of this is far fewer queries to the root-servers.
 
-.. versionchanged:: 4.0.0
-
-    Default is 'yes' now, was 'no' before 4.0.0
-
 .. _setting-save-parent-ns-set:
 
 ``save-parent-ns-set``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.7.0
 
-- Boolean
-- Default: yes
+-  Boolean
+-  Default: yes
+
+- YAML setting: :ref:`setting-yaml-recursor.save_parent_ns_set`
 
 If set, a parent (non-authoritative) ``NS`` set is saved if it contains more entries than a newly encountered child (authoritative) ``NS`` set for the same domain.
 The saved parent ``NS`` set is tried if resolution using the child ``NS`` set fails.
@@ -1893,31 +2196,39 @@ The saved parent ``NS`` set is tried if resolution using the child ``NS`` set fa
 .. _setting-security-poll-suffix:
 
 ``security-poll-suffix``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  String
 -  Default: secpoll.powerdns.com.
 
+- YAML setting: :ref:`setting-yaml-recursor.security_poll_suffix`
+
 Domain name from which to query security update notifications.
 Setting this to an empty string disables secpoll.
 
 .. _setting-serve-rfc1918:
 
 ``serve-rfc1918``
------------------
+~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: yes
 
+- YAML setting: :ref:`setting-yaml-recursor.serve_rfc1918`
+
 This makes the server authoritatively aware of: ``10.in-addr.arpa``, ``168.192.in-addr.arpa``, ``16-31.172.in-addr.arpa``, which saves load on the AS112 servers.
 Individual parts of these zones can still be loaded or forwarded.
 
 .. _setting-serve-stale-extensions:
 
 ``serve-stale-extensions``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.8.0
 
-- Integer
-- Default: 0
+-  Integer
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-recursor.serve_stale_extensions`
 
 Maximum number of times an expired record's TTL is extended by 30s when serving stale.
 Extension only occurs if a record cannot be refreshed.
@@ -1928,35 +2239,44 @@ See :ref:`serve-stale` for a description of the Serve Stale mechanism.
 .. _setting-server-down-max-fails:
 
 ``server-down-max-fails``
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 64
 
-If a server has not responded in any way this many times in a row, no longer send it any queries for `server-down-throttle-time`_ seconds.
-Afterwards, we will try a new packet, and if that also gets no response at all, we again throttle for `server-down-throttle-time`_ seconds.
+- YAML setting: :ref:`setting-yaml-recursor.server_down_max_fails`
+
+If a server has not responded in any way this many times in a row, no longer send it any queries for :ref:`setting-server-down-throttle-time` seconds.
+Afterwards, we will try a new packet, and if that also gets no response at all, we again throttle for :ref:`setting-server-down-throttle-time` seconds.
 Even a single response packet will drop the block.
 
 .. _setting-server-down-throttle-time:
 
 ``server-down-throttle-time``
------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 60
 
-Throttle a server that has failed to respond `server-down-max-fails`_ times for this many seconds.
+- YAML setting: :ref:`setting-yaml-recursor.server_down_throttle_time`
+
+Throttle a server that has failed to respond :ref:`setting-server-down-max-fails` times for this many seconds.
 
 .. _setting-server-id:
 
 ``server-id``
--------------
+~~~~~~~~~~~~~
+
 -  String
--  Default: The hostname of the server
+-  Default: *runtime determined*
+
+- YAML setting: :ref:`setting-yaml-recursor.server_id`
 
 The reply given by The PowerDNS recursor to a query for 'id.server' with its hostname, useful for in clusters.
 When a query contains the :rfc:`NSID EDNS0 Option <5001>`, this value is returned in the response as the NSID value.
 
 This setting can be used to override the answer given to these queries.
-Set to "disabled" to disable NSID and 'id.server' answers.
+Set to 'disabled' to disable NSID and 'id.server' answers.
 
 Query example (where 192.0.2.14 is your server):
 
@@ -1965,10 +2285,28 @@ Query example (where 192.0.2.14 is your server):
     dig @192.0.2.14 CHAOS TXT id.server.
     dig @192.0.2.14 example.com IN A +nsid
 
-``setgid``, ``setuid``
-----------------------
+.. _setting-setgid:
+
+``setgid``
+~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.setgid`
+
+PowerDNS can change its user and group id after binding to its socket.
+Can be used for better :doc:`security <security>`.
+
+.. _setting-setuid:
+
+``setuid``
+~~~~~~~~~~
+
 -  String
--  Default: unset
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.setuid`
 
 PowerDNS can change its user and group id after binding to its socket.
 Can be used for better :doc:`security <security>`.
@@ -1976,135 +2314,208 @@ Can be used for better :doc:`security <security>`.
 .. _setting-signature-inception-skew:
 
 ``signature-inception-skew``
-----------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.5
+.. versionchanged:: 4.2.0
+
+  Default is now 60, was 0 before.
 
 -  Integer
 -  Default: 60
 
-Allow the signature inception to be off by this number of seconds. Negative values are not allowed.
-
-.. versionchanged:: 4.2.0
+- YAML setting: :ref:`setting-yaml-dnssec.signature_inception_skew`
 
-    Default is now 60, was 0 before.
+Allow the signature inception to be off by this number of seconds. Negative values are not allowed.
 
 .. _setting-single-socket:
 
 ``single-socket``
------------------
+~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-outgoing.single_socket`
+
 Use only a single socket for outgoing queries.
 
 .. _setting-snmp-agent:
 
 ``snmp-agent``
---------------
+~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-snmp.agent`
+
 If set to true and PowerDNS has been compiled with SNMP support, it will register as an SNMP agent to provide statistics and be able to send traps.
 
 .. _setting-snmp-master-socket:
 
 ``snmp-master-socket``
-----------------------
-
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 .. deprecated:: 4.5.0
+
   Use :ref:`setting-snmp-daemon-socket`.
 
+-  String
+-  Default: (empty)
+
+- YAML setting does not exist
+
+
+
 .. _setting-snmp-daemon-socket:
 
 ``snmp-daemon-socket``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  String
--  Default: empty
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-snmp.daemon_socket`
 
 If not empty and ``snmp-agent`` is set to true, indicates how PowerDNS should contact the SNMP daemon to register as an SNMP agent.
 
 .. _setting-socket-dir:
 
 ``socket-dir``
---------------
--  Path
+~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.socket_dir`
 
 Where to store the control socket and pidfile.
 The default depends on ``LOCALSTATEDIR`` or the ``--with-socketdir`` setting when building (usually ``/var/run`` or ``/run``).
 
-When using `chroot`_ the default becomes to ``/``.
+When using :ref:`setting-chroot` the default becomes ``/``.
+
+.. _setting-socket-group:
+
+``socket-group``
+~~~~~~~~~~~~~~~~
 
-``socket-owner``, ``socket-group``, ``socket-mode``
----------------------------------------------------
-Owner, group and mode of the controlsocket.
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.socket_group`
+
+Group and mode of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+
+.. _setting-socket-mode:
+
+``socket-mode``
+~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.socket_mode`
+
+Mode of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+
+.. _setting-socket-owner:
+
+``socket-owner``
+~~~~~~~~~~~~~~~~
+
+-  String
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-recursor.socket_owner`
+
+Owner of the controlsocket.
 Owner and group can be specified by name, mode is in octal.
 
 .. _setting-spoof-nearmiss-max:
 
 ``spoof-nearmiss-max``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.5.0
+
   Older versions used 20 as the default value.
 
 -  Integer
 -  Default: 1
 
+- YAML setting: :ref:`setting-yaml-recursor.spoof_nearmiss_max`
+
 If set to non-zero, PowerDNS will assume it is being spoofed after seeing this many answers with the wrong id.
 
 .. _setting-stack-cache-size:
 
 ``stack-cache-size``
---------------------
+~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.9.0
 
 -  Integer
 -  Default: 100
 
+- YAML setting: :ref:`setting-yaml-recursor.stack_cache_size`
+
 Maximum number of mthread stacks that can be cached for later reuse, per thread. Caching these stacks reduces the CPU load at the cost of a slightly higher memory usage, each cached stack consuming `stack-size` bytes of memory.
 It makes no sense to cache more stacks than the value of `max-mthreads`, since there will never be more stacks than that in use at a given time.
 
 .. _setting-stack-size:
 
 ``stack-size``
---------------
+~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 200000
 
+- YAML setting: :ref:`setting-yaml-recursor.stack_size`
+
 Size in bytes of the stack of each mthread.
 
 .. _setting-statistics-interval:
 
 ``statistics-interval``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Integer
 -  Default: 1800
 
+- YAML setting: :ref:`setting-yaml-logging.statistics_interval`
+
 Interval between logging statistical summary on recursor performance.
 Use 0 to disable.
 
 .. _setting-stats-api-blacklist:
 
 ``stats-api-blacklist``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 .. deprecated:: 4.5.0
+
   Use :ref:`setting-stats-api-disabled-list`.
 
+-  Comma separated list of strings
+-  Default: 
+
+- YAML setting does not exist
+
+
+
 .. _setting-stats-api-disabled-list:
 
 ``stats-api-disabled-list``
----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  String
--  Default: "cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-*, ecs-v6-response-bits-*"
+-  Comma separated list of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*
+
+- YAML setting: :ref:`setting-yaml-recursor.stats_api_disabled_list`
 
 A list of comma-separated statistic names, that are disabled when retrieving the complete list of statistics via the API for performance reasons.
 These statistics can still be retrieved individually by specifically asking for it.
@@ -2112,38 +2523,58 @@ These statistics can still be retrieved individually by specifically asking for
 .. _setting-stats-carbon-blacklist:
 
 ``stats-carbon-blacklist``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 .. deprecated:: 4.5.0
+
   Use :ref:`setting-stats-carbon-disabled-list`.
 
+-  Comma separated list of strings
+-  Default: 
+
+- YAML setting does not exist
+
+
+
 .. _setting-stats-carbon-disabled-list:
 
 ``stats-carbon-disabled-list``
-------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  String
--  Default: "cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*"
+-  Comma separated list of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*
+
+- YAML setting: :ref:`setting-yaml-recursor.stats_carbon_disabled_list`
 
 A list of comma-separated statistic names, that are prevented from being exported via carbon for performance reasons.
 
 .. _setting-stats-rec-control-blacklist:
 
 ``stats-rec-control-blacklist``
--------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 .. deprecated:: 4.5.0
+
   Use :ref:`setting-stats-rec-control-disabled-list`.
 
+-  Comma separated list of strings
+-  Default: 
+
+- YAML setting does not exist
+
+
+
 .. _setting-stats-rec-control-disabled-list:
 
 ``stats-rec-control-disabled-list``
-------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  String
--  Default: "cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*"
+-  Comma separated list of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*
+
+- YAML setting: :ref:`setting-yaml-recursor.stats_rec_control_disabled_list`
 
 A list of comma-separated statistic names, that are disabled when retrieving the complete list of statistics via `rec_control get-all`, for performance reasons.
 These statistics can still be retrieved individually.
@@ -2151,51 +2582,68 @@ These statistics can still be retrieved individually.
 .. _setting-stats-ringbuffer-entries:
 
 ``stats-ringbuffer-entries``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 10000
 
+- YAML setting: :ref:`setting-yaml-recursor.stats_ringbuffer_entries`
+
 Number of entries in the remotes ringbuffer, which keeps statistics on who is querying your server.
 Can be read out using ``rec_control top-remotes``.
 
 .. _setting-stats-snmp-blacklist:
 
 ``stats-snmp-blacklist``
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 .. deprecated:: 4.5.0
+
   Use :ref:`setting-stats-snmp-disabled-list`.
 
+-  Comma separated list of strings
+-  Default: 
+
+- YAML setting does not exist
+
+
+
 .. _setting-stats-snmp-disabled-list:
 
 ``stats-snmp-disabled-list``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  String
--  Default: "cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-*, ecs-v6-response-bits-*"
+-  Comma separated list of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*
+
+- YAML setting: :ref:`setting-yaml-recursor.stats_snmp_disabled_list`
 
 A list of comma-separated statistic names, that are prevented from being exported via SNMP, for performance reasons.
 
 .. _setting-structured-logging:
 
 ``structured-logging``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
-- Boolean
-- Default: yes
+-  Boolean
+-  Default: yes
+
+- YAML setting: :ref:`setting-yaml-logging.structured_logging`
 
 Prefer structured logging when both an old style and a structured log messages is available.
 
 .. _setting-structured-logging-backend:
 
 ``structured-logging-backend``
-------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.8.0
 
-- String
-- Default: "default"
+-  String
+-  Default: default
+
+- YAML setting: :ref:`setting-yaml-logging.structured_logging_backend`
 
 The backend used for structured logging output.
 This setting must be set on the command line (``--structured-logging-backend=...``) to be effective.
@@ -2208,11 +2656,13 @@ Available backends are:
 .. _setting-tcp-fast-open:
 
 ``tcp-fast-open``
------------------
+~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.1.0
 
 -  Integer
--  Default: 0 (Disabled)
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-incoming.tcp_fast_open`
 
 Enable TCP Fast Open support, if available, on the listening sockets.
 The numerical value supplied is used as the queue size, 0 meaning disabled. See :ref:`tcp-fast-open-support`.
@@ -2220,75 +2670,91 @@ The numerical value supplied is used as the queue size, 0 meaning disabled. See
 .. _setting-tcp-fast-open-connect:
 
 ``tcp-fast-open-connect``
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
 -  Boolean
--  Default: no (disabled)
+-  Default: no
+
+- YAML setting: :ref:`setting-yaml-outgoing.tcp_fast_open_connect`
 
 Enable TCP Fast Open Connect support, if available, on the outgoing connections to authoritative servers. See :ref:`tcp-fast-open-support`.
 
 .. _setting-tcp-out-max-idle-ms:
 
 ``tcp-out-max-idle-ms``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
 -  Integer
--  Default : 10000
+-  Default: 10000
+
+- YAML setting: :ref:`setting-yaml-outgoing.tcp_max_idle_ms`
 
 Time outgoing TCP/DoT connections are left idle in milliseconds or 0 if no limit. After having been idle for this time, the connection is eligible for closing.
 
 .. _setting-tcp-out-max-idle-per-auth:
 
 ``tcp-out-max-idle-per-auth``
------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
 -  Integer
--  Default : 10
+-  Default: 10
+
+- YAML setting: :ref:`setting-yaml-outgoing.tcp_max_idle_per_auth`
 
 Maximum number of idle outgoing TCP/DoT connections to a specific IP per thread, 0 means do not keep idle connections open.
 
 .. _setting-tcp-out-max-queries:
 
 ``tcp-out-max-queries``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Integer
--  Default : 0
+-  Default: 0
+
+- YAML setting: :ref:`setting-yaml-outgoing.tcp_max_queries`
 
 Maximum total number of queries per outgoing TCP/DoT connection, 0 means no limit. After this number of queries, the connection is
 closed and a new one will be created if needed.
 
-.. versionadded:: 4.6.0
-
 .. _setting-tcp-out-max-idle-per-thread:
 
 ``tcp-out-max-idle-per-thread``
--------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
 -  Integer
--  Default : 100
+-  Default: 100
+
+- YAML setting: :ref:`setting-yaml-outgoing.tcp_max_idle_per_thread`
 
 Maximum number of idle outgoing TCP/DoT connections per thread, 0 means do not keep idle connections open.
 
 .. _setting-threads:
 
 ``threads``
------------
+~~~~~~~~~~~
+
 -  Integer
 -  Default: 2
 
+- YAML setting: :ref:`setting-yaml-recursor.threads`
+
 Spawn this number of threads on startup.
 
 .. _setting-trace:
 
 ``trace``
----------
--  String, one of ``no``, ``yes`` or ``fail``
--  Default: ``no``
+~~~~~~~~~
+
+-  String
+-  Default: no
+
+- YAML setting: :ref:`setting-yaml-logging.trace`
 
+One of ``no``, ``yes`` or ``fail``.
 If turned on, output impressive heaps of logging.
 May destroy performance under load.
 To log only queries resulting in a ``ServFail`` answer from the resolving process, this value can be set to ``fail``, but note that the performance impact is still large.
@@ -2297,55 +2763,64 @@ Also note that queries that do produce a result but with a failing DNSSEC valida
 .. _setting-udp-source-port-min:
 
 ``udp-source-port-min``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  Integer
 -  Default: 1024
 
+- YAML setting: :ref:`setting-yaml-outgoing.udp_source_port_min`
+
 This option sets the low limit of UDP port number to bind on.
 
-In combination with `udp-source-port-max`_ it configures the UDP
+In combination with :ref:`setting-udp-source-port-max` it configures the UDP
 port range to use. Port numbers are randomized within this range on
-initialization, and exceptions can be configured with `udp-source-port-avoid`_
+initialization, and exceptions can be configured with :ref:`setting-udp-source-port-avoid`
 
 .. _setting-udp-source-port-max:
 
 ``udp-source-port-max``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
 -  Integer
 -  Default: 65535
 
+- YAML setting: :ref:`setting-yaml-outgoing.udp_source_port_max`
+
 This option sets the maximum limit of UDP port number to bind on.
 
-See `udp-source-port-min`_.
+See :ref:`setting-udp-source-port-min`.
 
 .. _setting-udp-source-port-avoid:
 
 ``udp-source-port-avoid``
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
--  String
+-  Comma separated list of strings
 -  Default: 11211
 
+- YAML setting: :ref:`setting-yaml-outgoing.udp_source_port_avoid`
+
 A list of comma-separated UDP port numbers to avoid when binding.
 Ex: `5300,11211`
 
-See `udp-source-port-min`_.
+See :ref:`setting-udp-source-port-min`.
 
 .. _setting-udp-truncation-threshold:
 
 ``udp-truncation-threshold``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.2.0
-  Before 4.2.0, the default was 1680
+
+  Before 4.2.0, the default was 1680.
 
 -  Integer
 -  Default: 1232
 
+- YAML setting: :ref:`setting-yaml-incoming.udp_truncation_threshold`
+
 EDNS0 allows for large UDP response datagrams, which can potentially raise performance.
 Large responses however also have downsides in terms of reflection attacks.
 This setting limits the accepted size.
@@ -2356,11 +2831,13 @@ To know why 1232, see the note at :ref:`setting-edns-outgoing-bufsize`.
 .. _setting-unique-response-tracking:
 
 ``unique-response-tracking``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Boolean
-- Default: no (disabled)
+-  Boolean
+-  Default: no
+
+- YAML setting: :ref:`setting-yaml-nod.unique_response_tracking`
 
 Whether to track unique DNS responses, i.e. never seen before combinations
 of the triplet (query name, query type, RR[rrname, rrtype, rrdata]).
@@ -2377,11 +2854,13 @@ in the SBF using the unique-response-db-size setting can reduce FPs and FNs.
 .. _setting-unique-response-log:
 
 ``unique-response-log``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Boolean
-- Default: no (disabled)
+-  Boolean
+-  Default: yes
+
+- YAML setting: :ref:`setting-yaml-nod.unique_response_log`
 
 Whether to log when a unique response is detected. The log line
 looks something like:
@@ -2391,11 +2870,13 @@ Oct 24 12:11:27 Unique response observed: qname=foo.com qtype=A rrtype=AAAA rrna
 .. _setting-unique-response-db-size:
 
 ``unique-response-db-size``
----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Integer
-- Example: 67108864
+-  Integer
+-  Default: 67108864
+
+- YAML setting: :ref:`setting-yaml-nod.unique_response_db_size`
 
 The default size of the stable bloom filter used to store previously
 observed responses is 67108864. To change the number of cells, use this
@@ -2407,10 +2888,13 @@ have no effect unless you remove the existing files.
 .. _setting-unique-response-history-dir:
 
 ``unique-response-history-dir``
--------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- Path
+-  String
+-  Default: /usr/local/var/lib/pdns-recursor/udr
+
+- YAML setting: :ref:`setting-yaml-nod.unique_response_history_dir`
 
 This setting controls which directory is used to store the on-disk
 cache of previously observed responses.
@@ -2428,11 +2912,13 @@ unique-response-db-size, you must remove any files from this directory.
 .. _setting-unique-response-pb-tag:
 
 ``unique-response-pb-tag``
---------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
-- String
-- Default: pnds-udr
+-  String
+-  Default: pdns-udr
+
+- YAML setting: :ref:`setting-yaml-nod.unique_response_pb_tag`
 
 If protobuf is configured, then this tag will be added to all protobuf response messages when
 a unique DNS response is observed.
@@ -2440,25 +2926,25 @@ a unique DNS response is observed.
 .. _setting-use-incoming-edns-subnet:
 
 ``use-incoming-edns-subnet``
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
-Whether to process and pass along a received EDNS Client Subnet to authoritative servers.
-The ECS information will only be sent for netmasks and domains listed in `edns-subnet-allow-list`_ and will be truncated if the received scope exceeds `ecs-ipv4-bits`_ for IPv4 or `ecs-ipv6-bits`_ for IPv6.
-
-.. _setting-version:
+- YAML setting: :ref:`setting-yaml-incoming.use_incoming_edns_subnet`
 
-``version``
------------
-Print version of this binary. Useful for checking which version of the PowerDNS recursor is installed on a system.
+Whether to process and pass along a received EDNS Client Subnet to authoritative servers.
+The ECS information will only be sent for netmasks and domains listed in :ref:`setting-edns-subnet-allow-list` and will be truncated if the received scope exceeds :ref:`setting-ecs-ipv4-bits` for IPv4 or :ref:`setting-ecs-ipv6-bits` for IPv6.
 
 .. _setting-version-string:
 
 ``version-string``
-------------------
+~~~~~~~~~~~~~~~~~~
+
 -  String
--  Default: PowerDNS Recursor version number
+-  Default: *runtime determined*
+
+- YAML setting: :ref:`setting-yaml-recursor.version_string`
 
 By default, PowerDNS replies to the 'version.bind' query with its version number.
 Security conscious users may wish to override the reply PowerDNS issues.
@@ -2466,31 +2952,39 @@ Security conscious users may wish to override the reply PowerDNS issues.
 .. _setting-webserver:
 
 ``webserver``
--------------
+~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-webservice.webserver`
+
 Start the webserver (for REST API).
 
 .. _setting-webserver-address:
 
 ``webserver-address``
----------------------
--  IP Address
+~~~~~~~~~~~~~~~~~~~~~
+
+-  String
 -  Default: 127.0.0.1
 
+- YAML setting: :ref:`setting-yaml-webservice.address`
+
 IP address for the webserver to listen on.
 
 .. _setting-webserver-allow-from:
 
 ``webserver-allow-from``
-------------------------
--  IP addresses or netmasks, comma separated, negation supported
--  Default: 127.0.0.1,::1
-
+~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.1.0
 
-    Default is now 127.0.0.1,::1, was 0.0.0.0/0,::/0 before.
+  Default is now 127.0.0.1,::1, was 0.0.0.0/0,::/0 before.
+
+-  Comma separated list of IP addresses or subnets, negation supported
+-  Default: 127.0.0.1, ::1
+
+- YAML setting: :ref:`setting-yaml-webservice.allow_from`
 
 These IPs and subnets are allowed to access the webserver. Note that
 specifying an IP address without a netmask uses an implicit netmask
@@ -2499,30 +2993,35 @@ of /32 or /128.
 .. _setting-webserver-hash-plaintext-credentials:
 
 ``webserver-hash-plaintext-credentials``
-----------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.6.0
 
 -  Boolean
 -  Default: no
 
+- YAML setting: :ref:`setting-yaml-webservice.hash_plaintext_credentials`
+
 Whether passwords and API keys supplied in the configuration as plaintext should be hashed during startup, to prevent the plaintext versions from staying in memory. Doing so increases significantly the cost of verifying credentials and is thus disabled by default.
 Note that this option only applies to credentials stored in the configuration as plaintext, but hashed credentials are supported without enabling this option.
 
 .. _setting-webserver-loglevel:
 
 ``webserver-loglevel``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.2.0
 
--  String, one of "none", "normal", "detailed"
+-  String
 -  Default: normal
 
-The amount of logging the webserver must do. "none" means no useful webserver information will be logged.
-When set to "normal", the webserver will log a line per request that should be familiar::
+- YAML setting: :ref:`setting-yaml-webservice.loglevel`
+
+One of ``one``, ``normal``, ``detailed``.
+The amount of logging the webserver must do. 'none' means no useful webserver information will be logged.
+When set to 'normal', the webserver will log a line per request that should be familiar::
 
-  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 "GET /api/v1/servers/localhost/bla HTTP/1.1" 404 196
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 'GET /api/v1/servers/localhost/bla HTTP/1.1' 404 196
 
-When set to "detailed", all information about the request and response are logged::
+When set to 'detailed', all information about the request and response are logged::
 
   [webserver] e235780e-a5cf-415e-9326-9d33383e739e Request Details:
   [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Headers:
@@ -2543,7 +3042,7 @@ When set to "detailed", all information about the request and response are logge
   [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Server: PowerDNS/0.0.15896.0.gaba8bab3ab
   [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Full body:
   [webserver] e235780e-a5cf-415e-9326-9d33383e739e   <!html><title>Not Found</title><h1>Not Found</h1>
-  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 "GET /api/v1/servers/localhost/bla HTTP/1.1" 404 196
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 'GET /api/v1/servers/localhost/bla HTTP/1.1' 404 196
 
 The value between the hooks is a UUID that is generated for each request. This can be used to find all lines related to a single request.
 
@@ -2553,86 +3052,55 @@ The value between the hooks is a UUID that is generated for each request. This c
 .. _setting-webserver-password:
 
 ``webserver-password``
-----------------------
+~~~~~~~~~~~~~~~~~~~~~~
 .. versionchanged:: 4.6.0
+
   This setting now accepts a hashed and salted version.
 
 -  String
--  Default: unset
+-  Default: (empty)
+
+- YAML setting: :ref:`setting-yaml-webservice.password`
 
 Password required to access the webserver. Since 4.6.0 the password can be hashed and salted using ``rec_control hash-password`` instead of being present in the configuration in plaintext, but the plaintext version is still supported.
 
 .. _setting-webserver-port:
 
 ``webserver-port``
-------------------
+~~~~~~~~~~~~~~~~~~
+
 -  Integer
 -  Default: 8082
 
+- YAML setting: :ref:`setting-yaml-webservice.port`
+
 TCP port where the webserver should listen on.
 
 .. _setting-write-pid:
 
 ``write-pid``
--------------
+~~~~~~~~~~~~~
+
 -  Boolean
 -  Default: yes
 
-If a PID file should be written to `socket-dir`_
-
-.. _setting-xpf-allow-from:
-
-``xpf-allow-from``
-------------------
-.. versionadded:: 4.2.0
-.. deprecated:: 4.7.0
-
-.. versionchanged:: 4.8.0
-   This setting was removed.
-
--  IP addresses or netmasks, separated by commas
--  Default: empty
-
-.. note::
-  This is an experimental implementation of `draft-bellis-dnsop-xpf <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_.
-  This deprecated feature was removed in version 4.8.0.
-
-The server will trust XPF records found in queries sent from those netmasks (both IPv4 and IPv6),
-and will adjust queries' source and destination accordingly. This is especially useful when the recursor
-is placed behind a proxy like `dnsdist <https://dnsdist.org>`_.
-Note that the :ref:`setting-allow-from` setting is still applied to the original source address, and thus access restriction
-should be done on the proxy.
-
-.. _setting-xpf-rr-code:
+- YAML setting: :ref:`setting-yaml-recursor.write_pid`
 
-``xpf-rr-code``
----------------
-.. versionadded:: 4.2.0
-.. deprecated:: 4.7.0
-
-.. versionchanged:: 4.8.0
-   This setting was removed.
-
--  Integer
--  Default: 0
-
-.. note::
-  This is an experimental implementation of `draft-bellis-dnsop-xpf <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_.
-  This deprecated feature was removed in version 4.8.0.
-
-This option sets the resource record code to use for XPF records, as long as an official code has not been assigned to it.
-0 means that XPF is disabled.
+If a PID file should be written to :ref:`setting-socket-dir`
 
 .. _setting-x-dnssec-names:
 
 ``x-dnssec-names``
-------------------
+~~~~~~~~~~~~~~~~~~
 .. versionadded:: 4.5.0
 
--  Comma separated list of domain-names
+-  Comma separated list of strings
 -  Default: (empty)
 
+- YAML setting: :ref:`setting-yaml-dnssec.x_dnssec_names`
+
 List of names whose DNSSEC validation metrics will be counted in a separate set of metrics that start
 with ``x-dnssec-result-``.
 The names are suffix-matched.
 This can be used to not count known failing (test) name validations in the ordinary DNSSEC metrics.
+
index 8d3f19be7f01a0f39111c9e03b7512d8e38ec386..f1c43ed50f6a775cfa3a8484ebfe3b95eafac950 100644 (file)
@@ -246,7 +246,7 @@ Deprecated and changed settings
 
 Removed settings
 ^^^^^^^^^^^^^^^^
-- The :ref:`setting-query-local-address6` has been removed. It already was deprecated.
+- The ``query-local-address6`` setting has been removed. It already was deprecated.
 
 4.3.x to 4.4.0
 --------------
@@ -272,7 +272,7 @@ inconsistent results.
 Deprecated and changed settings
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 - The :ref:`setting-query-local-address` setting has been modified to be able to include both IPv4 and IPv6 addresses.
-- The :ref:`setting-query-local-address6` settings is now deprecated.
+- The ``query-local-address6`` setting is now deprecated.
 
 New settings
 ^^^^^^^^^^^^
@@ -324,8 +324,8 @@ New settings
 
 Two new settings have been added:
 
-- :ref:`setting-xpf-allow-from` can contain a list of IP addresses ranges from which `XPF (X-Proxied-For) <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_ records will be trusted.
-- :ref:`setting-xpf-rr-code` should list the number of the XPF record to use (in lieu of an assigned code).
+- ``xpf-allow-from`` can contain a list of IP addresses ranges from which `XPF (X-Proxied-For) <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_ records will be trusted.
+- ``setting-xpf-rr-code`` should list the number of the XPF record to use (in lieu of an assigned code).
 
 4.0.x to 4.1.0
 --------------
diff --git a/pdns/recursordist/docs/yamlsettings.rst b/pdns/recursordist/docs/yamlsettings.rst
new file mode 100644 (file)
index 0000000..c81f1dc
--- /dev/null
@@ -0,0 +1,3177 @@
+.. THIS IS A GENERATED FILE. DO NOT EDIT. SOURCE: see settings dir
+   START INCLUDE docs-new-preamble-in.rst
+
+PowerDNS Recursor New Style (YAML) Settings
+===========================================
+
+Each setting can appear on the command line, prefixed by ``--`` and using the old style name, or in configuration files.
+Settings on the command line are processed after the file-based settings are processed.
+
+.. note::
+   Starting with version 5.0.0., :program:`Recursor` supports a new YAML syntax for configuration files
+   as described here.
+   A configuration using the old style syntax can be converted to a YAML configuration using the instructions in :doc:`appendices/yamlconversion`.
+   In a future release support for the "old-style" settings will be dropped.
+
+
+YAML settings file
+------------------
+Please refer to e.g. `<https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html>`_
+for a description of YAML syntax.
+
+A :program:`Recursor` configuration file has several sections. For example, ``incoming`` for
+settings related to receiving queries and ``dnssec`` for settings related to DNSSEC processing.
+
+An example :program:`Recursor` YAML configuration file looks like:
+
+.. code-block:: yaml
+
+  dnssec:
+    log_bogus: true
+  incoming:
+    listen:
+      - 0.0.0.0:5301
+      - '[::]:5301'
+  recursor:
+    extended_resolution_errors: true
+    forward_zones:
+      - zone: example.com
+        forwarders:
+          - 127.0.0.1:5301
+  outgoing:
+    query_local_address:
+      - 0.0.0.0
+      - '::'
+  logging:
+    loglevel: 6
+
+Take care when listing IPv6 addresses, as characters used for these are special to YAML.
+If in doubt, quote any string containing ``:``, ``[`` or ``]`` and use (online) tools to check your YAML syntax.
+Specify an empty sequence using ``[]``.
+
+The main setting file is called ``recursor.yml`` and will be processed first.
+This settings file might refer to other files via the `recursor.include_dir`_ setting.
+The next section will describe how settings specified in multiple files are merged.
+
+Merging multiple setting files
+------------------------------
+If `recursor.include_dir`_ is set, all ``.yml`` files in it will be processed in alphabetical order, modifying the  settings processed so far.
+
+For simple values like an boolean or number setting, a value in the processed file will overwrite an existing setting.
+
+For values of type sequence, the new value will *replace* the existing value if the existing value is equal to the ``default`` or if the new value is marked with the ``!override`` tag.
+Otherwise, the existing value will be *extended* with the new value by appending the new sequence to the existing.
+
+For example, with the above example ``recursor.yml`` and an include directory containing a file ``extra.yml``:
+
+.. code-block:: yaml
+
+  dnssec:
+    log_bogus: false
+  recursor:
+    forward_zones:
+      - zone: example.net
+        forwarders:
+          - '::1'
+  outgoing:
+     query_local_address: !override
+       - 0.0.0.0
+     dont_query: []
+
+After merging, ``dnssec.log_bogus`` will be ``false``, the sequence of ``recursor.forward_zones`` will contain 2 zones and the ``outgoing`` addresses used will contain one entry, as the ``extra.yml`` entry has overwritten the existing one.
+
+``outgoing.dont-query`` has a non-empty sequence as default value. The main ``recursor.yml`` did not set it, so before processing ``extra.yml`` had the default value.
+After processing ``extra.yml`` the value will be set to the empty sequence, as existing default values are overwritten by new values.
+
+.. warning::
+   The merging process does not process values deeper than the second level.
+   For example if the main ``recursor.yml`` specified a forward zone
+
+   .. code-block:: yaml
+
+     forward_zones:
+       - zone: example.net
+         forwarders:
+           - '::1'
+
+   and another settings file contains
+
+   .. code-block:: yaml
+
+     forward_zones:
+       - zone: example.net
+         forwarders:
+           - '::2'
+
+   The result will *not* be a a single forward with two IP addresses, but two entries for ``example.net``.
+   It depends on the specific setting how the sequence is processed further.
+   In the future we might add a check for this case.
+
+Socket Address
+^^^^^^^^^^^^^^
+A socket address is either an IP or and IP:port combination
+For example:
+
+.. code-block:: yaml
+
+   some_key: 127.0.0.1
+   another_key: '[::1]:8080'
+
+Subnet
+^^^^^^
+A subnet is a single IP address or an IP address followed by a slash and a prefix length.
+If no prefix length is specified, ``/32`` or ``/128`` is assumed, indicating a single IP address.
+Subnets can also be prefixed with a ``!``, specifying negation.
+This can be used to deny addresses from a previously allowed range.
+
+For example, ``alow-from`` takes a sequence of subnets:
+
+.. code-block:: yaml
+
+   allow_from:
+     - '2001:DB8::/32'
+     - 128.66.0.0/16
+     - !128.66.1.2
+
+In this case the address ``128.66.1.2`` is excluded from the addresses allowed access.
+
+Forward Zone
+^^^^^^^^^^^^
+A forward zone is defined as:
+
+.. code-block:: yaml
+
+  zone: zonename
+  forwarders:
+    - Socket Address
+    - ...
+  recurse: Boolean, default false
+  allow_notify:  Boolean, default false
+
+An example of a ``forward_zones`` entry, which consists of a sequence of forward zone entries:
+
+.. code-block:: yaml
+
+  - zone: example1.com
+    forwarders:
+      - 127.0.0.1
+      - 127.0.0.1:5353
+      - '[::1]53'
+  - zone: example2.com
+    forwarders:
+      - '::1'
+    recurse: true
+    notify_allowed: true
+
+
+Auth Zone
+^^^^^^^^^
+A auth zone is defined as:
+
+.. code-block:: yaml
+
+  zone: name
+  file: filename
+
+An example of a ``auth_zones`` entry, consisting of a sequence of auth zones:
+
+.. code-block:: yaml
+
+   auth_zones:
+     - zone: example.com
+       file: zones/example.com.zone
+     - zone: example.net
+       file: zones/example.net.zone
+
+The YAML settings
+-----------------
+
+.. END INCLUDE docs-new-preamble-in.rst
+
+.. _setting-yaml-carbon.instance:
+
+``carbon.instance``
+^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``recursor``
+
+- Old style setting: :ref:`setting-carbon-instance`
+
+Change the instance or third string of the metric key. The default is recursor.
+
+.. _setting-yaml-carbon.interval:
+
+``carbon.interval``
+^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``30``
+
+- Old style setting: :ref:`setting-carbon-interval`
+
+If sending carbon updates, this is the interval between them in seconds.
+See :doc:`metrics`.
+
+.. _setting-yaml-carbon.ns:
+
+``carbon.ns``
+^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``pdns``
+
+- Old style setting: :ref:`setting-carbon-namespace`
+
+Change the namespace or first string of the metric key. The default is pdns.
+
+.. _setting-yaml-carbon.ourname:
+
+``carbon.ourname``
+^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-carbon-ourname`
+
+If sending carbon updates, if set, this will override our hostname.
+Be careful not to include any dots in this setting, unless you know what you are doing.
+See :ref:`metricscarbon`.
+
+.. _setting-yaml-carbon.server:
+
+``carbon.server``
+^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Socket Address`_ (IP or IP:port combinations)
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-carbon-server`
+
+Will send all available metrics to these servers via the carbon protocol, which is used by graphite and metronome.
+See :doc:`metrics`.
+
+.. _setting-yaml-dnssec.aggressive_cache_min_nsec3_hit_ratio:
+
+``dnssec.aggressive_cache_min_nsec3_hit_ratio``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.9.0
+
+-  Integer
+-  Default: ``2000``
+
+- Old style setting: :ref:`setting-aggressive-cache-min-nsec3-hit-ratio`
+
+The limit for which to put NSEC3 records into the aggressive cache.
+A value of ``n`` means that an NSEC3 record is only put into the aggressive cache if the estimated probability of a random name hitting the NSEC3 record is higher than ``1/n``.
+A higher ``n`` will cause more records to be put into the aggressive cache, e.g. a value of 4000 will cause records to be put in the aggressive cache even if the estimated probability of hitting them is twice as low as would be the case for ``n=2000``.
+A value of 0 means no NSEC3 records will be put into the aggressive cache.
+
+For large zones the effectiveness of the NSEC3 cache is reduced since each NSEC3 record only covers a randomly distributed subset of all possible names.
+This setting avoids doing unnecessary work for such large zones.
+
+.. _setting-yaml-dnssec.aggressive_nsec_cache_size:
+
+``dnssec.aggressive_nsec_cache_size``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Integer
+-  Default: ``100000``
+
+- Old style setting: :ref:`setting-aggressive-nsec-cache-size`
+
+The number of records to cache in the aggressive cache. If set to a value greater than 0, the recursor will cache NSEC and NSEC3 records to generate negative answers, as defined in :rfc:`8198`.
+To use this, DNSSEC processing or validation must be enabled by setting :ref:`setting-yaml-dnssec.validation` to ``process``, ``log-fail`` or ``validate``.
+
+.. _setting-yaml-dnssec.disabled_algorithms:
+
+``dnssec.disabled_algorithms``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.9.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-dnssec-disabled-algorithms`
+
+A list of DNSSEC algorithm numbers that should be considered disabled.
+These algorithms will not be used to validate DNSSEC signatures.
+Zones (only) signed with these algorithms will be considered ``Insecure``.
+
+If this setting is empty (the default), :program:`Recursor` will determine which algorithms to disable automatically.
+This is done for specific algorithms only, currently algorithms 5 (``RSASHA1``) and 7 (``RSASHA1NSEC3SHA1``).
+
+This is important on systems that have a default strict crypto policy, like RHEL9 derived systems.
+On such systems not disabling some algorithms (or changing the security policy) will make affected zones to be considered ``Bogus`` as using these algorithms fails.
+
+.. _setting-yaml-dnssec.log_bogus:
+
+``dnssec.log_bogus``
+^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-dnssec-log-bogus`
+
+Log every DNSSEC validation failure.
+**Note**: This is not logged per-query but every time records are validated as Bogus.
+
+.. _setting-yaml-dnssec.nsec3_max_iterations:
+
+``dnssec.nsec3_max_iterations``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+.. versionchanged:: 4.5.2
+
+  Default is now 150, was 2500 before.
+
+-  Integer
+-  Default: ``150``
+
+- Old style setting: :ref:`setting-nsec3-max-iterations`
+
+Maximum number of iterations allowed for an NSEC3 record.
+If an answer containing an NSEC3 record with more iterations is received, its DNSSEC validation status is treated as Insecure.
+
+.. _setting-yaml-dnssec.signature_inception_skew:
+
+``dnssec.signature_inception_skew``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.5
+.. versionchanged:: 4.2.0
+
+  Default is now 60, was 0 before.
+
+-  Integer
+-  Default: ``60``
+
+- Old style setting: :ref:`setting-signature-inception-skew`
+
+Allow the signature inception to be off by this number of seconds. Negative values are not allowed.
+
+.. _setting-yaml-dnssec.validation:
+
+``dnssec.validation``
+^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.0.0
+.. versionchanged:: 4.5.0
+
+  The default changed from ``process-no-validate`` to ``process``
+
+-  String
+-  Default: ``process``
+
+- Old style setting: :ref:`setting-dnssec`
+
+One of ``off``, ``process-no-validate``, ``process``, ``log-fail``, ``validate``
+
+Set the mode for DNSSEC processing, as detailed in :doc:`dnssec`.
+
+``off``
+   No DNSSEC processing whatsoever.
+   Ignore DO-bits in queries, don't request any DNSSEC information from authoritative servers.
+   This behaviour is similar to PowerDNS Recursor pre-4.0.
+``process-no-validate``
+   Respond with DNSSEC records to clients that ask for it, set the DO bit on all outgoing queries.
+   Don't do any validation.
+``process``
+   Respond with DNSSEC records to clients that ask for it, set the DO bit on all outgoing queries.
+   Do validation for clients that request it (by means of the AD- bit or DO-bit in the query).
+``log-fail``
+   Similar behaviour to ``process``, but validate RRSIGs on responses and log bogus responses.
+``validate``
+   Full blown DNSSEC validation. Send SERVFAIL to clients on bogus responses.
+
+.. _setting-yaml-dnssec.x_dnssec_names:
+
+``dnssec.x_dnssec_names``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-x-dnssec-names`
+
+List of names whose DNSSEC validation metrics will be counted in a separate set of metrics that start
+with ``x-dnssec-result-``.
+The names are suffix-matched.
+This can be used to not count known failing (test) name validations in the ordinary DNSSEC metrics.
+
+.. _setting-yaml-ecs.add_for:
+
+``ecs.add_for``
+^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[0.0.0.0/0, ::/0, !127.0.0.0/8, !10.0.0.0/8, !100.64.0.0/10, !169.254.0.0/16, !192.168.0.0/16, !172.16.0.0/12, !::1/128, !fc00::/7, !fe80::/10]``
+
+- Old style setting: :ref:`setting-ecs-add-for`
+
+List of requestor netmasks for which the requestor IP Address should be used as the :rfc:`EDNS Client Subnet <7871>` for outgoing queries. Outgoing queries for requestors that do not match this list will use the :ref:`setting-yaml-ecs.scope_zero_address` instead.
+Valid incoming ECS values from :ref:`setting-yaml-incoming.use_incoming_edns_subnet` are not replaced.
+
+Regardless of the value of this setting, ECS values are only sent for outgoing queries matching the conditions in the :ref:`setting-yaml-outgoing.edns_subnet_allow_list` setting. This setting only controls the actual value being sent.
+
+This defaults to not using the requestor address inside RFC1918 and similar 'private' IP address spaces.
+
+.. _setting-yaml-ecs.cache_limit_ttl:
+
+``ecs.cache_limit_ttl``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.12
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-ecs-cache-limit-ttl`
+
+The minimum TTL for an ECS-specific answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-ipv4-cache-bits`` or ``ecs-ipv6-cache-bits``.
+That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
+
+.. _setting-yaml-ecs.ipv4_bits:
+
+``ecs.ipv4_bits``
+^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Integer
+-  Default: ``24``
+
+- Old style setting: :ref:`setting-ecs-ipv4-bits`
+
+Number of bits of client IPv4 address to pass when sending EDNS Client Subnet address information.
+
+.. _setting-yaml-ecs.ipv4_cache_bits:
+
+``ecs.ipv4_cache_bits``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.12
+
+-  Integer
+-  Default: ``24``
+
+- Old style setting: :ref:`setting-ecs-ipv4-cache-bits`
+
+Maximum number of bits of client IPv4 address used by the authoritative server (as indicated by the EDNS Client Subnet scope in the answer) for an answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-cache-limit-ttl``.
+That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
+
+.. _setting-yaml-ecs.ipv4_never_cache:
+
+``ecs.ipv4_never_cache``
+^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-ecs-ipv4-never-cache`
+
+When set, never cache replies carrying EDNS IPv4 Client Subnet scope in the record cache.
+In this case the decision made by ```ecs-ipv4-cache-bits`` and ``ecs-cache-limit-ttl`` is no longer relevant.
+
+.. _setting-yaml-ecs.ipv6_bits:
+
+``ecs.ipv6_bits``
+^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Integer
+-  Default: ``56``
+
+- Old style setting: :ref:`setting-ecs-ipv6-bits`
+
+Number of bits of client IPv6 address to pass when sending EDNS Client Subnet address information.
+
+.. _setting-yaml-ecs.ipv6_cache_bits:
+
+``ecs.ipv6_cache_bits``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.12
+
+-  Integer
+-  Default: ``56``
+
+- Old style setting: :ref:`setting-ecs-ipv6-cache-bits`
+
+Maximum number of bits of client IPv6 address used by the authoritative server (as indicated by the EDNS Client Subnet scope in the answer) for an answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-cache-limit-ttl``.
+That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
+
+.. _setting-yaml-ecs.ipv6_never_cache:
+
+``ecs.ipv6_never_cache``
+^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-ecs-ipv6-never-cache`
+
+When set, never cache replies carrying EDNS IPv6 Client Subnet scope in the record cache.
+In this case the decision made by ```ecs-ipv6-cache-bits`` and ``ecs-cache-limit-ttl`` is no longer relevant.
+
+.. _setting-yaml-ecs.minimum_ttl_override:
+
+``ecs.minimum_ttl_override``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.5.0
+
+  Old versions used default 0.
+
+-  Integer
+-  Default: ``1``
+
+- Old style setting: :ref:`setting-ecs-minimum-ttl-override`
+
+This setting artificially raises the TTLs of records in the ANSWER section of ECS-specific answers to be at least this long.
+Setting this to a value greater than 1 technically is an RFC violation, but might improve performance a lot.
+Using a value of 0 impacts performance of TTL 0 records greatly, since it forces the recursor to contact
+authoritative servers every time a client requests them.
+Can be set at runtime using ``rec_control set-ecs-minimum-ttl 3600``.
+
+.. _setting-yaml-ecs.scope_zero_address:
+
+``ecs.scope_zero_address``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-ecs-scope-zero-address`
+
+The IP address sent via EDNS Client Subnet to authoritative servers listed in
+:ref:`setting-yaml-outgoing.edns_subnet_allow_list` when :ref:`setting-yaml-incoming.use_incoming_edns_subnet` is set and the query has
+an ECS source prefix-length set to 0.
+The default is to look for the first usable (not an ``any`` one) address in
+:ref:`setting-yaml-outgoing.source_address` (starting with IPv4). If no suitable address is
+found, the recursor fallbacks to sending 127.0.0.1.
+
+.. _setting-yaml-incoming.allow_from:
+
+``incoming.allow_from``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10]``
+
+- Old style setting: :ref:`setting-allow-from`
+
+Netmasks (both IPv4 and IPv6) that are allowed to use the server.
+The default allows access only from :rfc:`1918` private IP addresses.
+An empty value means no checking is done, all clients are allowed.
+Due to the aggressive nature of the internet these days, it is highly recommended to not open up the recursor for the entire internet.
+Questions from IP addresses not listed here are ignored and do not get an answer.
+
+When the Proxy Protocol is enabled (see :ref:`setting-yaml-incoming.proxy_protocol_from`), the recursor will check the address of the client IP advertised in the Proxy Protocol header instead of the one of the proxy.
+
+Note that specifying an IP address without a netmask uses an implicit netmask of /32 or /128.
+
+.. _setting-yaml-incoming.allow_from_file:
+
+``incoming.allow_from_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-allow-from-file`
+
+Like :ref:`setting-yaml-incoming.allow_from`, except reading a sequence of `Subnet`_ from file.
+Overrides the :ref:`setting-yaml-incoming.allow_from` setting. Example content of th specified file:
+
+.. code-block:: yaml
+
+ - 127.0.01
+ - ::1
+
+.. _setting-yaml-incoming.allow_notify_for:
+
+``incoming.allow_notify_for``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-allow-notify-for`
+
+Domain names specified in this list are used to permit incoming
+NOTIFY operations to wipe any cache entries that match the domain
+name. If this list is empty, all NOTIFY operations will be ignored.
+
+.. _setting-yaml-incoming.allow_notify_for_file:
+
+``incoming.allow_notify_for_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-allow-notify-for-file`
+
+Like :ref:`setting-yaml-incoming.allow_notify_for`, except reading a sequence of names from file. Example contents of specified file:
+
+.. code-block:: yaml
+
+ - example.com
+ - example.org
+
+.. _setting-yaml-incoming.allow_notify_from:
+
+``incoming.allow_notify_from``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-allow-notify-from`
+
+Subnets (both IPv4 and IPv6) that are allowed to issue NOTIFY operations
+to the server.  NOTIFY operations from IP addresses not listed here are
+ignored and do not get an answer.
+
+When the Proxy Protocol is enabled (see :ref:`setting-yaml-incoming.proxy_protocol_from`), the
+recursor will check the address of the client IP advertised in the
+Proxy Protocol header instead of the one of the proxy.
+
+Note that specifying an IP address without a netmask uses an implicit
+netmask of /32 or /128.
+
+NOTIFY operations received from a client listed in one of these netmasks
+will be accepted and used to wipe any cache entries whose zones match
+the zone specified in the NOTIFY operation, but only if that zone (or
+one of its parents) is included in :ref:`setting-yaml-incoming.allow_notify_for`,
+:ref:`setting-yaml-incoming.allow_notify_for_file`, or :ref:`setting-yaml-recursor.forward_zones_file` with a ``allow_notify`` set to ``true``.
+
+.. _setting-yaml-incoming.allow_notify_from_file:
+
+``incoming.allow_notify_from_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-allow-notify-from-file`
+
+Like :ref:`setting-yaml-incoming.allow_notify_from`, except reading a sequence of `Subnet`_ from file.
+
+.. _setting-yaml-incoming.distribution_load_factor:
+
+``incoming.distribution_load_factor``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.12
+
+-  Double
+-  Default: ``0.0``
+
+- Old style setting: :ref:`setting-distribution-load-factor`
+
+If :ref:`setting-yaml-incoming.pdns_distributes_queries` is set and this setting is set to another value
+than 0, the distributor thread will use a bounded load-balancing algorithm while
+distributing queries to worker threads, making sure that no thread is assigned
+more queries than distribution-load-factor times the average number of queries
+currently processed by all the workers.
+For example, with a value of 1.25, no server should get more than 125 % of the
+average load. This helps making sure that all the workers have roughly the same
+share of queries, even if the incoming traffic is very skewed, with a larger
+number of requests asking for the same qname.
+
+.. _setting-yaml-incoming.distribution_pipe_buffer_size:
+
+``incoming.distribution_pipe_buffer_size``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-distribution-pipe-buffer-size`
+
+Size in bytes of the internal buffer of the pipe used by the distributor to pass incoming queries to a worker thread.
+Requires support for `F_SETPIPE_SZ` which is present in Linux since 2.6.35. The actual size might be rounded up to
+a multiple of a page size. 0 means that the OS default size is used.
+A large buffer might allow the recursor to deal with very short-lived load spikes during which a worker thread gets
+overloaded, but it will be at the cost of an increased latency.
+
+.. _setting-yaml-incoming.distributor_threads:
+
+``incoming.distributor_threads``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: 1 if :ref:`setting-pdns-distributes-queries` is set, 0 otherwise
+
+- Old style setting: :ref:`setting-distributor-threads`
+
+If :ref:`setting-yaml-incoming.pdns_distributes_queries` is set, spawn this number of distributor threads on startup. Distributor threads
+handle incoming queries and distribute them to other threads based on a hash of the query.
+
+.. _setting-yaml-incoming.edns_padding_from:
+
+``incoming.edns_padding_from``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-edns-padding-from`
+
+List of netmasks (proxy IP in case of proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses, provided that :ref:`setting-yaml-incoming.edns_padding_mode` applies.
+
+.. _setting-yaml-incoming.edns_padding_mode:
+
+``incoming.edns_padding_mode``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  String
+-  Default: ``padded-queries-only``
+
+- Old style setting: :ref:`setting-edns-padding-mode`
+
+One of ``always``, ``padded-queries-only``.
+Whether to add EDNS padding to all responses (``always``) or only to responses for queries containing the EDNS padding option (``padded-queries-only``, the default).
+In both modes, padding will only be added to responses for queries coming from :ref:`setting-yaml-incoming.edns_padding_from` sources.
+
+.. _setting-yaml-incoming.edns_padding_tag:
+
+``incoming.edns_padding_tag``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Integer
+-  Default: ``7830``
+
+- Old style setting: :ref:`setting-edns-padding-tag`
+
+The packetcache tag to use for padded responses, to prevent a client not allowed by the :ref::`setting-edns-padding-from` list to be served a cached answer generated for an allowed one. This
+effectively divides the packet cache in two when :ref:`setting-yaml-incoming.edns_padding_from` is used. Note that this will not override a tag set from one of the ``Lua`` hooks.
+
+.. _setting-yaml-incoming.gettag_needs_edns_options:
+
+``incoming.gettag_needs_edns_options``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-gettag-needs-edns-options`
+
+If set, EDNS options in incoming queries are extracted and passed to the :func:`gettag` hook in the ``ednsoptions`` table.
+
+.. _setting-yaml-incoming.listen:
+
+``incoming.listen``
+^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Socket Address`_ (IP or IP:port combinations)
+-  Default: ``[127.0.0.1]``
+
+- Old style setting: :ref:`setting-local-address`
+
+Local IP addresses to which we bind. Each address specified can
+include a port number; if no port is included then the
+:ref:`setting-yaml-incoming.port` port will be used for that address. If a
+port number is specified, it must be separated from the address with a
+':'; for an IPv6 address the address must be enclosed in square
+brackets.
+
+Examples::
+
+  local-address=127.0.0.1 ::1
+  local-address=0.0.0.0:5353
+  local-address=[::]:8053
+  local-address=127.0.0.1:53, [::1]:5353
+
+.. _setting-yaml-incoming.max_concurrent_requests_per_tcp_connection:
+
+``incoming.max_concurrent_requests_per_tcp_connection``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.3.0
+
+-  Integer
+-  Default: ``10``
+
+- Old style setting: :ref:`setting-max-concurrent-requests-per-tcp-connection`
+
+Maximum number of incoming requests handled concurrently per tcp
+connection. This number must be larger than 0 and smaller than 65536
+and also smaller than `max-mthreads`.
+
+.. _setting-yaml-incoming.max_tcp_clients:
+
+``incoming.max_tcp_clients``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``128``
+
+- Old style setting: :ref:`setting-max-tcp-clients`
+
+Maximum number of simultaneous incoming TCP connections allowed.
+
+.. _setting-yaml-incoming.max_tcp_per_client:
+
+``incoming.max_tcp_per_client``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-max-tcp-per-client`
+
+Maximum number of simultaneous incoming TCP connections allowed per client (remote IP address).
+ 0 means unlimited.
+
+.. _setting-yaml-incoming.max_tcp_queries_per_connection:
+
+``incoming.max_tcp_queries_per_connection``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-max-tcp-queries-per-connection`
+
+Maximum number of DNS queries in a TCP connection.
+0 means unlimited.
+
+.. _setting-yaml-incoming.max_udp_queries_per_round:
+
+``incoming.max_udp_queries_per_round``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.4
+
+-  Integer
+-  Default: ``10000``
+
+- Old style setting: :ref:`setting-max-udp-queries-per-round`
+
+Under heavy load the recursor might be busy processing incoming UDP queries for a long while before there is no more of these, and might therefore
+neglect scheduling new ``mthreads``, handling responses from authoritative servers or responding to :doc:`rec_control <manpages/rec_control.1>`
+requests.
+This setting caps the maximum number of incoming UDP DNS queries processed in a single round of looping on ``recvmsg()`` after being woken up by the multiplexer, before
+returning back to normal processing and handling other events.
+
+.. _setting-yaml-incoming.non_local_bind:
+
+``incoming.non_local_bind``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-non-local-bind`
+
+Bind to addresses even if one or more of the :ref:`setting-yaml-incoming.listen`'s do not exist on this server.
+Setting this option will enable the needed socket options to allow binding to non-local addresses.
+This feature is intended to facilitate ip-failover setups, but it may also mask configuration issues and for this reason it is disabled by default.
+
+.. _setting-yaml-incoming.pdns_distributes_queries:
+
+``incoming.pdns_distributes_queries``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.9.0
+
+  Default changed to ``no``, previously it was ``yes``.
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-pdns-distributes-queries`
+
+If set, PowerDNS will use distinct threads to listen to client sockets and distribute that work to worker-threads using a hash of the query.
+This feature should maximize the cache hit ratio on versions before 4.9.0.
+To use more than one thread set :ref:`setting-yaml-incoming.distributor_threads` in version 4.2.0 or newer.
+Enabling should improve performance on systems where :ref:`setting-yaml-incoming.reuseport` does not have the effect of
+balancing the queries evenly over multiple worker threads.
+
+.. _setting-yaml-incoming.port:
+
+``incoming.port``
+^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``53``
+
+- Old style setting: :ref:`setting-local-port`
+
+Local port to bind to.
+If an address in :ref:`setting-yaml-incoming.listen` does not have an explicit port, this port is used.
+
+.. _setting-yaml-incoming.proxy_protocol_from:
+
+``incoming.proxy_protocol_from``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.4.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-proxy-protocol-from`
+
+Ranges that are required to send a Proxy Protocol version 2 header in front of UDP and TCP queries, to pass the original source and destination addresses and ports to the recursor, as well as custom values.
+Queries that are not prefixed with such a header will not be accepted from clients in these ranges. Queries prefixed by headers from clients that are not listed in these ranges will be dropped.
+
+Note that once a Proxy Protocol header has been received, the source address from the proxy header instead of the address of the proxy will be checked against the :ref:`setting-yaml-incoming.allow_from` ACL.
+
+The dnsdist docs have `more information about the PROXY protocol <https://dnsdist.org/advanced/passing-source-address.html#proxy-protocol>`_.
+
+.. _setting-yaml-incoming.proxy_protocol_maximum_size:
+
+``incoming.proxy_protocol_maximum_size``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.4.0
+
+-  Integer
+-  Default: ``512``
+
+- Old style setting: :ref:`setting-proxy-protocol-maximum-size`
+
+The maximum size, in bytes, of a Proxy Protocol payload (header, addresses and ports, and TLV values). Queries with a larger payload will be dropped.
+
+.. _setting-yaml-incoming.reuseport:
+
+``incoming.reuseport``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.9.0
+
+  The default is changed to ``yes``, previously it was ``no``. If ``SO_REUSEPORT`` support is not available, the setting defaults to ``no``.
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-reuseport`
+
+If ``SO_REUSEPORT`` support is available, allows multiple threads and processes to open listening sockets for the same port.
+
+Since 4.1.0, when :ref:`setting-yaml-incoming.pdns_distributes_queries` is disabled and :ref:`setting-yaml-incoming.reuseport` is enabled, every worker-thread will open a separate listening socket to let the kernel distribute the incoming queries instead of running a distributor thread (which could otherwise be a bottleneck) and avoiding thundering herd issues, thus leading to much higher performance on multi-core boxes.
+
+.. _setting-yaml-incoming.tcp_fast_open:
+
+``incoming.tcp_fast_open``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-tcp-fast-open`
+
+Enable TCP Fast Open support, if available, on the listening sockets.
+The numerical value supplied is used as the queue size, 0 meaning disabled. See :ref:`tcp-fast-open-support`.
+
+.. _setting-yaml-incoming.tcp_timeout:
+
+``incoming.tcp_timeout``
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``2``
+
+- Old style setting: :ref:`setting-client-tcp-timeout`
+
+Time to wait for data from TCP clients.
+
+.. _setting-yaml-incoming.udp_truncation_threshold:
+
+``incoming.udp_truncation_threshold``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.2.0
+
+  Before 4.2.0, the default was 1680.
+
+-  Integer
+-  Default: ``1232``
+
+- Old style setting: :ref:`setting-udp-truncation-threshold`
+
+EDNS0 allows for large UDP response datagrams, which can potentially raise performance.
+Large responses however also have downsides in terms of reflection attacks.
+This setting limits the accepted size.
+Maximum value is 65535, but values above 4096 should probably not be attempted.
+
+To know why 1232, see the note at :ref:`setting-yaml-outgoing.edns_bufsize`.
+
+.. _setting-yaml-incoming.use_incoming_edns_subnet:
+
+``incoming.use_incoming_edns_subnet``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-use-incoming-edns-subnet`
+
+Whether to process and pass along a received EDNS Client Subnet to authoritative servers.
+The ECS information will only be sent for netmasks and domains listed in :ref:`setting-yaml-outgoing.edns_subnet_allow_list` and will be truncated if the received scope exceeds :ref:`setting-yaml-ecs.ipv4_bits` for IPv4 or :ref:`setting-yaml-ecs.ipv6_bits` for IPv6.
+
+.. _setting-yaml-logging.common_errors:
+
+``logging.common_errors``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-log-common-errors`
+
+Some DNS errors occur rather frequently and are no cause for alarm.
+
+.. _setting-yaml-logging.disable_syslog:
+
+``logging.disable_syslog``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-disable-syslog`
+
+Do not log to syslog, only to stdout.
+Use this setting when running inside a supervisor that handles logging (like systemd).
+**Note**: do not use this setting in combination with :ref:`setting-yaml-recursor.daemon` as all logging will disappear.
+
+.. _setting-yaml-logging.facility:
+
+``logging.facility``
+^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-logging-facility`
+
+If set to a digit, logging is performed under this LOCAL facility.
+See :ref:`logging`.
+Do not pass names like 'local0'!
+
+.. _setting-yaml-logging.loglevel:
+
+``logging.loglevel``
+^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``6``
+
+- Old style setting: :ref:`setting-loglevel`
+
+Amount of logging. The higher the number, the more lines logged.
+Corresponds to 'syslog' level values (e.g. 0 = emergency, 1 = alert, 2 = critical, 3 = error, 4 = warning, 5 = notice, 6 = info, 7 = debug).
+Each level includes itself plus the lower levels before it.
+Not recommended to set this below 3.
+
+.. _setting-yaml-logging.protobuf_use_kernel_timestamp:
+
+``logging.protobuf_use_kernel_timestamp``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-protobuf-use-kernel-timestamp`
+
+Whether to compute the latency of responses in protobuf messages using the timestamp set by the kernel when the query packet was received (when available), instead of computing it based on the moment we start processing the query.
+
+.. _setting-yaml-logging.quiet:
+
+``logging.quiet``
+^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``yes``
+
+- Old style setting: :ref:`setting-quiet`
+
+Don't log queries.
+
+.. _setting-yaml-logging.rpz_changes:
+
+``logging.rpz_changes``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-log-rpz-changes`
+
+Log additions and removals to RPZ zones at Info (6) level instead of Debug (7).
+
+.. _setting-yaml-logging.statistics_interval:
+
+``logging.statistics_interval``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Integer
+-  Default: ``1800``
+
+- Old style setting: :ref:`setting-statistics-interval`
+
+Interval between logging statistical summary on recursor performance.
+Use 0 to disable.
+
+.. _setting-yaml-logging.structured_logging:
+
+``logging.structured_logging``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-structured-logging`
+
+Prefer structured logging when both an old style and a structured log messages is available.
+
+.. _setting-yaml-logging.structured_logging_backend:
+
+``logging.structured_logging_backend``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.8.0
+
+-  String
+-  Default: ``default``
+
+- Old style setting: :ref:`setting-structured-logging-backend`
+
+The backend used for structured logging output.
+This setting must be set on the command line (``--structured-logging-backend=...``) to be effective.
+Available backends are:
+
+- ``default``: use the traditional logging system to output structured logging information.
+- ``systemd-journal``: use systemd-journal.
+  When using this backend, provide ``-o verbose`` or simular output option to ``journalctl`` to view the full information.
+
+.. _setting-yaml-logging.timestamp:
+
+``logging.timestamp``
+^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-log-timestamp`
+
+
+
+.. _setting-yaml-logging.trace:
+
+``logging.trace``
+^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``no``
+
+- Old style setting: :ref:`setting-trace`
+
+One of ``no``, ``yes`` or ``fail``.
+If turned on, output impressive heaps of logging.
+May destroy performance under load.
+To log only queries resulting in a ``ServFail`` answer from the resolving process, this value can be set to ``fail``, but note that the performance impact is still large.
+Also note that queries that do produce a result but with a failing DNSSEC validation are not written to the log
+
+.. _setting-yaml-nod.db_size:
+
+``nod.db_size``
+^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``67108864``
+
+- Old style setting: :ref:`setting-new-domain-db-size`
+
+The default size of the stable bloom filter used to store previously
+observed domains is 67108864. To change the number of cells, use this
+setting. For each cell, the SBF uses 1 bit of memory, and one byte of
+disk for the persistent file.
+If there are already persistent files saved to disk, this setting will
+have no effect unless you remove the existing files.
+
+.. _setting-yaml-nod.history_dir:
+
+``nod.history_dir``
+^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``/usr/local/var/lib/pdns-recursor/nod``
+
+- Old style setting: :ref:`setting-new-domain-history-dir`
+
+This setting controls which directory is used to store the on-disk
+cache of previously observed domains.
+
+The default depends on ``LOCALSTATEDIR`` when building the software.
+Usually this comes down to ``/var/lib/pdns-recursor/nod`` or ``/usr/local/var/lib/pdns-recursor/nod``).
+
+The newly observed domain feature uses a stable bloom filter to store
+a history of previously observed domains. The data structure is
+synchronized to disk every 10 minutes, and is also initialized from
+disk on startup. This ensures that previously observed domains are
+preserved across recursor restarts.
+If you change the new-domain-db-size setting, you must remove any files
+from this directory.
+
+.. _setting-yaml-nod.ignore_list:
+
+``nod.ignore_list``
+^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-new-domain-ignore-list`
+
+This setting is a list of all domains (and implicitly all subdomains)
+that will never be considered a new domain. For example, if the domain
+'xyz123.tv' is in the list, then 'foo.bar.xyz123.tv' will never be
+considered a new domain. One use-case for the ignore list is to never
+reveal details of internal subdomains via the new-domain-lookup
+feature.
+
+.. _setting-yaml-nod.log:
+
+``nod.log``
+^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-new-domain-log`
+
+If a newly observed domain is detected, log that domain in the
+recursor log file. The log line looks something like::
+
+ Jul 18 11:31:25 Newly observed domain nod=sdfoijdfio.com
+
+.. _setting-yaml-nod.lookup:
+
+``nod.lookup``
+^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-new-domain-lookup`
+
+If a domain is specified, then each time a newly observed domain is
+detected, the recursor will perform an A record lookup of '<newly
+observed domain>.<lookup domain>'. For example if 'new-domain-lookup'
+is configured as 'nod.powerdns.com', and a new domain 'xyz123.tv' is
+detected, then an A record lookup will be made for
+'xyz123.tv.nod.powerdns.com'. This feature gives a way to share the
+newly observed domain with partners, vendors or security teams. The
+result of the DNS lookup will be ignored by the recursor.
+
+.. _setting-yaml-nod.pb_tag:
+
+``nod.pb_tag``
+^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``pdns-nod``
+
+- Old style setting: :ref:`setting-new-domain-pb-tag`
+
+If protobuf is configured, then this tag will be added to all protobuf response messages when
+a new domain is observed.
+
+.. _setting-yaml-nod.tracking:
+
+``nod.tracking``
+^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-new-domain-tracking`
+
+Whether to track newly observed domains, i.e. never seen before. This
+is a probabilistic algorithm, using a stable bloom filter to store
+records of previously seen domains. When enabled for the first time,
+all domains will appear to be newly observed, so the feature is best
+left enabled for e.g. a week or longer before using the results. Note
+that this feature is optional and must be enabled at compile-time,
+thus it may not be available in all pre-built packages.
+If protobuf is enabled and configured, then the newly observed domain
+status will appear as a flag in Response messages.
+
+.. _setting-yaml-nod.unique_response_db_size:
+
+``nod.unique_response_db_size``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``67108864``
+
+- Old style setting: :ref:`setting-unique-response-db-size`
+
+The default size of the stable bloom filter used to store previously
+observed responses is 67108864. To change the number of cells, use this
+setting. For each cell, the SBF uses 1 bit of memory, and one byte of
+disk for the persistent file.
+If there are already persistent files saved to disk, this setting will
+have no effect unless you remove the existing files.
+
+.. _setting-yaml-nod.unique_response_history_dir:
+
+``nod.unique_response_history_dir``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``/usr/local/var/lib/pdns-recursor/udr``
+
+- Old style setting: :ref:`setting-unique-response-history-dir`
+
+This setting controls which directory is used to store the on-disk
+cache of previously observed responses.
+
+The default depends on ``LOCALSTATEDIR`` when building the software.
+Usually this comes down to ``/var/lib/pdns-recursor/udr`` or ``/usr/local/var/lib/pdns-recursor/udr``).
+
+The newly observed domain feature uses a stable bloom filter to store
+a history of previously observed responses. The data structure is
+synchronized to disk every 10 minutes, and is also initialized from
+disk on startup. This ensures that previously observed responses are
+preserved across recursor restarts. If you change the
+unique-response-db-size, you must remove any files from this directory.
+
+.. _setting-yaml-nod.unique_response_log:
+
+``nod.unique_response_log``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-unique-response-log`
+
+Whether to log when a unique response is detected. The log line
+looks something like:
+
+Oct 24 12:11:27 Unique response observed: qname=foo.com qtype=A rrtype=AAAA rrname=foo.com rrcontent=1.2.3.4
+
+.. _setting-yaml-nod.unique_response_pb_tag:
+
+``nod.unique_response_pb_tag``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``pdns-udr``
+
+- Old style setting: :ref:`setting-unique-response-pb-tag`
+
+If protobuf is configured, then this tag will be added to all protobuf response messages when
+a unique DNS response is observed.
+
+.. _setting-yaml-nod.unique_response_tracking:
+
+``nod.unique_response_tracking``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-unique-response-tracking`
+
+Whether to track unique DNS responses, i.e. never seen before combinations
+of the triplet (query name, query type, RR[rrname, rrtype, rrdata]).
+This can be useful for tracking potentially suspicious domains and
+behaviour, e.g. DNS fast-flux.
+If protobuf is enabled and configured, then the Protobuf Response message
+will contain a flag with udr set to true for each RR that is considered
+unique, i.e. never seen before.
+This feature uses a probabilistic data structure (stable bloom filter) to
+track unique responses, which can have false positives as well as false
+negatives, thus it is a best-effort feature. Increasing the number of cells
+in the SBF using the unique-response-db-size setting can reduce FPs and FNs.
+
+.. _setting-yaml-outgoing.dont_query:
+
+``outgoing.dont_query``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10, 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 240.0.0.0/4, ::/96, ::ffff:0:0/96, 100::/64, 2001:db8::/32]``
+
+- Old style setting: :ref:`setting-dont-query`
+
+The DNS is a public database, but sometimes contains delegations to private IP addresses, like for example 127.0.0.1.
+This can have odd effects, depending on your network, and may even be a security risk.
+Therefore, the PowerDNS Recursor by default does not query private space IP addresses.
+This setting can be used to expand or reduce the limitations.
+
+Queries for names in forward zones and to addresses as configured in any of the settings :ref:`setting-yaml-recursor.forward_zones`, :ref:`setting-yaml-recursor.forward_zones_file` or :ref:`setting-yaml-recursor.forward_zones_recurse` are performed regardless of these limitations.
+
+.. _setting-yaml-outgoing.dont_throttle_names:
+
+``outgoing.dont_throttle_names``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-dont-throttle-names`
+
+When an authoritative server does not answer a query or sends a reply the recursor does not like, it is throttled.
+Any servers' name suffix-matching the supplied names will never be throttled.
+
+.. warning::
+  Most servers on the internet do not respond for a good reason (overloaded or unreachable), ``dont-throttle-names`` could make this load on the upstream server even higher, resulting in further service degradation.
+
+.. _setting-yaml-outgoing.dont_throttle_netmasks:
+
+``outgoing.dont_throttle_netmasks``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-dont-throttle-netmasks`
+
+When an authoritative server does not answer a query or sends a reply the recursor does not like, it is throttled.
+Any servers matching the supplied netmasks will never be throttled.
+
+This can come in handy on lossy networks when forwarding, where the same server is configured multiple times (e.g. with ``forward-zones-recurse=example.com=192.0.2.1;192.0.2.1``).
+By default, the PowerDNS Recursor would throttle the 'first' server on a timeout and hence not retry the 'second' one.
+In this case, ``dont-throttle-netmasks`` could be set to ``192.0.2.1``.
+
+.. warning::
+  Most servers on the internet do not respond for a good reason (overloaded or unreachable), ``dont-throttle-netmasks`` could make this load on the upstream server even higher, resulting in further service degradation.
+
+.. _setting-yaml-outgoing.dot_to_auth_names:
+
+``outgoing.dot_to_auth_names``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-dot-to-auth-names`
+
+Force DoT to the listed authoritative nameservers. For this to work, DoT support has to be compiled in.
+Currently, the certificate is not checked for validity in any way.
+
+.. _setting-yaml-outgoing.dot_to_port_853:
+
+``outgoing.dot_to_port_853``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-dot-to-port-853`
+
+Enable DoT to forwarders that specify port 853.
+
+.. _setting-yaml-outgoing.edns_bufsize:
+
+``outgoing.edns_bufsize``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.2.0
+
+  Before 4.2.0, the default was 1680
+
+-  Integer
+-  Default: ``1232``
+
+- Old style setting: :ref:`setting-edns-outgoing-bufsize`
+
+.. note:: Why 1232?
+
+  1232 is the largest number of payload bytes that can fit in the smallest IPv6 packet.
+  IPv6 has a minimum MTU of 1280 bytes (:rfc:`RFC 8200, section 5 <8200#section-5>`), minus 40 bytes for the IPv6 header, minus 8 bytes for the UDP header gives 1232, the maximum payload size for the DNS response.
+
+This is the value set for the EDNS0 buffer size in outgoing packets.
+Lower this if you experience timeouts.
+
+.. _setting-yaml-outgoing.edns_padding:
+
+``outgoing.edns_padding``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.8.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-edns-padding-out`
+
+Whether to add EDNS padding to outgoing DoT queries.
+
+.. _setting-yaml-outgoing.edns_subnet_allow_list:
+
+``outgoing.edns_subnet_allow_list``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-edns-subnet-allow-list`
+
+List of netmasks and domains that :rfc:`EDNS Client Subnet <7871>` should be enabled for in outgoing queries.
+
+For example, an EDNS Client Subnet option containing the address of the initial requestor (but see :ref:`setting-yaml-ecs.add_for`) will be added to an outgoing query sent to server 192.0.2.1 for domain X if 192.0.2.1 matches one of the supplied netmasks, or if X matches one of the supplied domains.
+The initial requestor address will be truncated to 24 bits for IPv4 (see :ref:`setting-yaml-ecs.ipv4_bits`) and to 56 bits for IPv6 (see :ref:`setting-yaml-ecs.ipv6_bits`), as recommended in the privacy section of RFC 7871.
+
+
+Note that this setting describes the destination of outgoing queries, not the sources of incoming queries, nor the subnets described in the EDNS Client Subnet option.
+
+By default, this option is empty, meaning no EDNS Client Subnet information is sent.
+
+.. _setting-yaml-outgoing.lowercase:
+
+``outgoing.lowercase``
+^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-lowercase-outgoing`
+
+Set to true to lowercase the outgoing queries.
+When set to 'no' (the default) a query from a client using mixed case in the DNS labels (such as a user entering mixed-case names or `draft-vixie-dnsext-dns0x20-00 <http://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00>`_), PowerDNS preserves the case of the query.
+Broken authoritative servers might give a wrong or broken answer on this encoding.
+Setting ``lowercase-outgoing`` to 'yes' makes the PowerDNS Recursor lowercase all the labels in the query to the authoritative servers, but still return the proper case to the client requesting.
+
+.. _setting-yaml-outgoing.max_busy_dot_probes:
+
+``outgoing.max_busy_dot_probes``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.7.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-max-busy-dot-probes`
+
+Limit the maximum number of simultaneous DoT probes the Recursor will schedule.
+The default value 0 means no DoT probes are scheduled.
+
+DoT probes are used to check if an authoritative server's IP address supports DoT.
+If the probe determines an IP address supports DoT, the Recursor will use DoT to contact it for subsequent queries until a failure occurs.
+After a failure, the Recursor will stop using DoT for that specific IP address for a while.
+The results of probes are remembered and can be viewed by the ``rec_control dump-dot-probe-map`` command.
+If the maximum number of pending probes is reached, no probes will be scheduled, even if no DoT status is known for an address.
+If the result of a probe is not yet available, the Recursor will contact the authoritative server in the regular way, unless an authoritative server is configured to be contacted over DoT always using :ref:`setting-yaml-outgoing.dot_to_auth_names`.
+In that case no probe will be scheduled.
+
+.. note::
+  DoT probing is an experimental feature.
+  Please test thoroughly to determine if it is suitable in your specific production environment before enabling.
+
+.. _setting-yaml-outgoing.network_timeout:
+
+``outgoing.network_timeout``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``1500``
+
+- Old style setting: :ref:`setting-network-timeout`
+
+Number of milliseconds to wait for a remote authoritative server to respond.
+
+.. _setting-yaml-outgoing.single_socket:
+
+``outgoing.single_socket``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-single-socket`
+
+Use only a single socket for outgoing queries.
+
+.. _setting-yaml-outgoing.source_address:
+
+``outgoing.source_address``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.4.0
+
+  IPv6 addresses can be set with this option as well.
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[0.0.0.0]``
+
+- Old style setting: :ref:`setting-query-local-address`
+
+Send out local queries from this address, or addresses. By adding multiple
+addresses, increased spoofing resilience is achieved. When no address of a certain
+address family is configured, there are *no* queries sent with that address family.
+In the default configuration this means that IPv6 is not used for outgoing queries.
+
+.. _setting-yaml-outgoing.tcp_fast_open_connect:
+
+``outgoing.tcp_fast_open_connect``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-tcp-fast-open-connect`
+
+Enable TCP Fast Open Connect support, if available, on the outgoing connections to authoritative servers. See :ref:`tcp-fast-open-support`.
+
+.. _setting-yaml-outgoing.tcp_max_idle_ms:
+
+``outgoing.tcp_max_idle_ms``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Integer
+-  Default: ``10000``
+
+- Old style setting: :ref:`setting-tcp-out-max-idle-ms`
+
+Time outgoing TCP/DoT connections are left idle in milliseconds or 0 if no limit. After having been idle for this time, the connection is eligible for closing.
+
+.. _setting-yaml-outgoing.tcp_max_idle_per_auth:
+
+``outgoing.tcp_max_idle_per_auth``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Integer
+-  Default: ``10``
+
+- Old style setting: :ref:`setting-tcp-out-max-idle-per-auth`
+
+Maximum number of idle outgoing TCP/DoT connections to a specific IP per thread, 0 means do not keep idle connections open.
+
+.. _setting-yaml-outgoing.tcp_max_idle_per_thread:
+
+``outgoing.tcp_max_idle_per_thread``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Integer
+-  Default: ``100``
+
+- Old style setting: :ref:`setting-tcp-out-max-idle-per-thread`
+
+Maximum number of idle outgoing TCP/DoT connections per thread, 0 means do not keep idle connections open.
+
+.. _setting-yaml-outgoing.tcp_max_queries:
+
+``outgoing.tcp_max_queries``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-tcp-out-max-queries`
+
+Maximum total number of queries per outgoing TCP/DoT connection, 0 means no limit. After this number of queries, the connection is
+closed and a new one will be created if needed.
+
+.. _setting-yaml-outgoing.udp_source_port_avoid:
+
+``outgoing.udp_source_port_avoid``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Sequence of strings
+-  Default: ``[11211]``
+
+- Old style setting: :ref:`setting-udp-source-port-avoid`
+
+A sequence of UDP port numbers to avoid when binding. For example:
+
+.. code-block:: yaml
+
+ outgoing:
+   udp_source_port_avoid:
+   - 5300
+   - 11211
+
+See :ref:`setting-yaml-outgoing.udp_source_port_min`.
+
+.. _setting-yaml-outgoing.udp_source_port_max:
+
+``outgoing.udp_source_port_max``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``65535``
+
+- Old style setting: :ref:`setting-udp-source-port-max`
+
+This option sets the maximum limit of UDP port number to bind on.
+
+See :ref:`setting-yaml-outgoing.udp_source_port_min`.
+
+.. _setting-yaml-outgoing.udp_source_port_min:
+
+``outgoing.udp_source_port_min``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``1024``
+
+- Old style setting: :ref:`setting-udp-source-port-min`
+
+This option sets the low limit of UDP port number to bind on.
+
+In combination with :ref:`setting-yaml-outgoing.udp_source_port_max` it configures the UDP
+port range to use. Port numbers are randomized within this range on
+initialization, and exceptions can be configured with :ref:`setting-yaml-outgoing.udp_source_port_avoid`
+
+.. _setting-yaml-packetcache.disable:
+
+``packetcache.disable``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-disable-packetcache`
+
+Turn off the packet cache. Useful when running with Lua scripts that can not be cached, though individual query caching can be controlled from Lua as well.
+
+.. _setting-yaml-packetcache.max_entries:
+
+``packetcache.max_entries``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``500000``
+
+- Old style setting: :ref:`setting-max-packetcache-entries`
+
+Maximum number of Packet Cache entries. Sharded and shared by all threads since 4.9.0.
+
+.. _setting-yaml-packetcache.negative_ttl:
+
+``packetcache.negative_ttl``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.9.0
+
+-  Integer
+-  Default: ``60``
+
+- Old style setting: :ref:`setting-packetcache-negative-ttl`
+
+Maximum number of seconds to cache an ``NxDomain`` or ``NoData`` answer in the packetcache.
+This setting's maximum is capped to :ref:`setting-yaml-packetcache.ttl`.
+i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-negative-ttl`` at the default will lower ``packetcache-negative-ttl`` to ``15``.
+
+.. _setting-yaml-packetcache.servfail_ttl:
+
+``packetcache.servfail_ttl``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+'versionchanged': ('4.0.0', "This setting's maximum is capped to :ref:`setting-yaml-packetcache.ttl`.
+    i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-servfail-ttl`` at the default will lower ``packetcache-servfail-ttl`` to ``15``.")
+
+-  Integer
+-  Default: ``60``
+
+- Old style setting: :ref:`setting-packetcache-servfail-ttl`
+
+Maximum number of seconds to cache an answer indicating a failure to resolve in the packet cache.
+Before version 4.6.0 only ``ServFail`` answers were considered as such. Starting with 4.6.0, all responses with a code other than ``NoError`` and ``NXDomain``, or without records in the answer and authority sections, are considered as a failure to resolve.
+Since 4.9.0, negative answers are handled separately from resolving failures.
+
+.. _setting-yaml-packetcache.shards:
+
+``packetcache.shards``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.9.0
+
+-  Integer
+-  Default: ``1024``
+
+- Old style setting: :ref:`setting-packetcache-shards`
+
+Sets the number of shards in the packet cache. If you have high contention as reported by ``packetcache-contented/packetcache-acquired``,
+you can try to enlarge this value or run with fewer threads.
+
+.. _setting-yaml-packetcache.ttl:
+
+``packetcache.ttl``
+^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.9.0
+
+  The default was changed from 3600 (1 hour) to 86400 (24 hours).
+
+-  Integer
+-  Default: ``86400``
+
+- Old style setting: :ref:`setting-packetcache-ttl`
+
+Maximum number of seconds to cache an item in the packet cache, no matter what the original TTL specified.
+
+.. _setting-yaml-recordcache.locked_ttl_perc:
+
+``recordcache.locked_ttl_perc``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.8.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-record-cache-locked-ttl-perc`
+
+Replace record sets in the record cache only after this percentage of the original TTL has passed.
+The PowerDNS Recursor already has several mechanisms to protect against spoofing attempts.
+This adds an extra layer of protection---as it limits the window of time cache updates are accepted---at the cost of a less efficient record cache.
+
+The default value of 0 means no extra locking occurs.
+When non-zero, record sets received (e.g. in the Additional Section) will not replace existing record sets in the record cache until the given percentage of the original TTL has expired.
+A value of 100 means only expired record sets will be replaced.
+
+There are a few cases where records will be replaced anyway:
+
+- Record sets that are expired will always be replaced.
+- Authoritative record sets will replace unauthoritative record sets unless DNSSEC validation of the new record set failed.
+- If the new record set belongs to a DNSSEC-secure zone and successfully passed validation it will replace an existing entry.
+- Record sets produced by :ref:`setting-yaml-recordcache.refresh_on_ttl_perc` tasks will also replace existing record sets.
+
+.. _setting-yaml-recordcache.max_cache_bogus_ttl:
+
+``recordcache.max_cache_bogus_ttl``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``3600``
+
+- Old style setting: :ref:`setting-max-cache-bogus-ttl`
+
+Maximum number of seconds to cache an item in the DNS cache (negative or positive) if its DNSSEC validation failed, no matter what the original TTL specified, to reduce the impact of a broken domain.
+
+.. _setting-yaml-recordcache.max_entries:
+
+``recordcache.max_entries``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``1000000``
+
+- Old style setting: :ref:`setting-max-cache-entries`
+
+Maximum number of DNS record cache entries, shared by all threads since 4.4.0.
+Each entry associates a name and type with a record set.
+The size of the negative cache is 10% of this number.
+
+.. _setting-yaml-recordcache.max_negative_ttl:
+
+``recordcache.max_negative_ttl``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``3600``
+
+- Old style setting: :ref:`setting-max-negative-ttl`
+
+A query for which there is authoritatively no answer is cached to quickly deny a record's existence later on, without putting a heavy load on the remote server.
+In practice, caches can become saturated with hundreds of thousands of hosts which are tried only once.
+This setting, which defaults to 3600 seconds, puts a maximum on the amount of time negative entries are cached.
+
+.. _setting-yaml-recordcache.max_ttl:
+
+``recordcache.max_ttl``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.1.0
+
+  The minimum value of this setting is 15. i.e. setting this to lower than 15 will make this value 15.
+
+-  Integer
+-  Default: ``86400``
+
+- Old style setting: :ref:`setting-max-cache-ttl`
+
+Maximum number of seconds to cache an item in the DNS cache, no matter what the original TTL specified.
+This value also controls the refresh period of cached root data.
+See :ref:`handling-of-root-hints` for more information on this.
+
+.. _setting-yaml-recordcache.refresh_on_ttl_perc:
+
+``recordcache.refresh_on_ttl_perc``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-refresh-on-ttl-perc`
+
+Sets the 'refresh almost expired' percentage of the record cache. Whenever a record is fetched from the packet or record cache
+and only ``refresh-on-ttl-perc`` percent or less of its original TTL is left, a task is queued to refetch the name/type combination to
+update the record cache. In most cases this causes future queries to always see a non-expired record cache entry.
+A typical value is 10. If the value is zero, this functionality is disabled.
+
+.. _setting-yaml-recordcache.shards:
+
+``recordcache.shards``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.4.0
+
+-  Integer
+-  Default: ``1024``
+
+- Old style setting: :ref:`setting-record-cache-shards`
+
+Sets the number of shards in the record cache. If you have high
+contention as reported by
+``record-cache-contented/record-cache-acquired``, you can try to
+enlarge this value or run with fewer threads.
+
+.. _setting-yaml-recursor.allow_trust_anchor_query:
+
+``recursor.allow_trust_anchor_query``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.3.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-allow-trust-anchor-query`
+
+Allow ``trustanchor.server CH TXT`` and ``negativetrustanchor.server CH TXT`` queries to view the configured :doc:`DNSSEC <dnssec>` (negative) trust anchors.
+
+.. _setting-yaml-recursor.any_to_tcp:
+
+``recursor.any_to_tcp``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-any-to-tcp`
+
+Answer questions for the ANY type on UDP with a truncated packet that refers the remote server to TCP.
+Useful for mitigating ANY reflection attacks.
+
+.. _setting-yaml-recursor.auth_zones:
+
+``recursor.auth_zones``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Auth Zone`_
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-auth-zones`
+
+Zones read from these files (in BIND format) are served authoritatively (but without the AA bit set in responses).
+DNSSEC is not supported. Example:
+
+.. code-block:: yaml
+
+ recursor:
+    auth-zones:
+    - zone: example.org
+      file: /var/zones/example.org
+    - zone: powerdns.com
+      file: /var/zones/powerdns.com
+
+.. _setting-yaml-recursor.chroot:
+
+``recursor.chroot``
+^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-chroot`
+
+If set, chroot to this directory for more security.
+This is not recommended; instead, we recommend containing PowerDNS using operating system features.
+We ship systemd unit files with our packages to make this easy.
+
+Make sure that ``/dev/log`` is available from within the chroot.
+Logging will silently fail over time otherwise (on logrotate).
+
+When using ``chroot``, all other paths (except for :ref:`setting-yaml-recursor.config_dir`) set in the configuration are relative to the new root.
+
+When running on a system where systemd manages services, ``chroot`` does not work out of the box, as PowerDNS cannot use the ``NOTIFY_SOCKET``.
+Either do not ``chroot`` on these systems or set the 'Type' of this service to 'simple' instead of 'notify' (refer to the systemd documentation on how to modify unit-files).
+
+.. _setting-yaml-recursor.config_dir:
+
+``recursor.config_dir``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``/etc/powerdns``
+
+- Old style setting: :ref:`setting-config-dir`
+
+Location of configuration directory (``recursor.conf``).
+Usually ``/etc/powerdns``, but this depends on ``SYSCONFDIR`` during compile-time.
+
+.. _setting-yaml-recursor.config_name:
+
+``recursor.config_name``
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-config-name`
+
+When running multiple recursors on the same server, read settings from :file:`recursor-{name}.conf`, this will also rename the binary image.
+
+.. _setting-yaml-recursor.cpu_map:
+
+``recursor.cpu_map``
+^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-cpu-map`
+
+Set CPU affinity for threads, asking the scheduler to run those threads on a single CPU, or a set of CPUs.
+This parameter accepts a space separated list of thread-id=cpu-id, or thread-id=cpu-id-1,cpu-id-2,...,cpu-id-N.
+For example, to make the worker thread 0 run on CPU id 0 and the worker thread 1 on CPUs 1 and 2::
+
+.. code-block:: yaml
+
+  recursor:
+    cpu_map: 0=0 1=1,2
+
+The thread handling the control channel, the webserver and other internal stuff has been assigned id 0, the distributor
+threads if any are assigned id 1 and counting, and the worker threads follow behind.
+The number of distributor threads is determined by :ref:`setting-yaml-incoming.distributor_threads`, the number of worker threads is determined by the :ref:`setting-yaml-recursor.threads` setting.
+
+This parameter is only available if the OS provides the ``pthread_setaffinity_np()`` function.
+
+Note that depending on the configuration the Recursor can start more threads.
+Typically these threads will sleep most of the time.
+These threads cannot be specified in this setting as their thread-ids are left unspecified.
+
+.. _setting-yaml-recursor.daemon:
+
+``recursor.daemon``
+^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.0.0
+
+  Default is now ``no``, was ``yes`` before.
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-daemon`
+
+Operate in the background.
+
+.. _setting-yaml-recursor.dns64_prefix:
+
+``recursor.dns64_prefix``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.4.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-dns64-prefix`
+
+Enable DNS64 (:rfc:`6147`) support using the supplied /96 IPv6 prefix. This will generate 'fake' ``AAAA`` records for names
+with only ``A`` records, as well as 'fake' ``PTR`` records to make sure that reverse lookup of DNS64-generated IPv6 addresses
+generate the right name.
+See :doc:`dns64` for more flexible but slower alternatives using Lua.
+
+.. _setting-yaml-recursor.entropy_source:
+
+``recursor.entropy_source``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``/dev/urandom``
+
+- Old style setting: :ref:`setting-entropy-source`
+
+PowerDNS can read entropy from a (hardware) source.
+This is used for generating random numbers which are very hard to predict.
+Generally on UNIX platforms, this source will be ``/dev/urandom``, which will always supply random numbers, even if entropy is lacking.
+Change to ``/dev/random`` if PowerDNS should block waiting for enough entropy to arrive.
+
+.. _setting-yaml-recursor.etc_hosts_file:
+
+``recursor.etc_hosts_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``/etc/hosts``
+
+- Old style setting: :ref:`setting-etc-hosts-file`
+
+The path to the /etc/hosts file, or equivalent.
+This file can be used to serve data authoritatively using :ref:`setting-yaml-recursor.export_etc_hosts`.
+
+.. _setting-yaml-recursor.event_trace_enabled:
+
+``recursor.event_trace_enabled``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-event-trace-enabled`
+
+Enable the recording and logging of ref:`event traces`. This is an experimental feature and subject to change.
+Possible values are 0: (disabled), 1 (add information to protobuf logging messages) and 2 (write to log) and 3 (both).
+
+.. _setting-yaml-recursor.export_etc_hosts:
+
+``recursor.export_etc_hosts``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-export-etc-hosts`
+
+If set, this flag will export the host names and IP addresses mentioned in ``/etc/hosts``.
+
+.. _setting-yaml-recursor.export_etc_hosts_search_suffix:
+
+``recursor.export_etc_hosts_search_suffix``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-export-etc-hosts-search-suffix`
+
+If set, all hostnames in the :ref:`setting-yaml-recursor.export_etc_hosts` file are loaded in canonical form, based on this suffix, unless the name contains a '.', in which case the name is unchanged.
+So an entry called 'pc' with ``export-etc-hosts-search-suffix='home.com'`` will lead to the generation of 'pc.home.com' within the recursor.
+An entry called 'server1.home' will be stored as 'server1.home', regardless of this setting.
+
+.. _setting-yaml-recursor.extended_resolution_errors:
+
+``recursor.extended_resolution_errors``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-extended-resolution-errors`
+
+If set, the recursor will add an EDNS Extended Error (:rfc:`8914`) to responses when resolution failed, like DNSSEC validation errors, explaining the reason it failed. This setting is not needed to allow setting custom error codes from Lua or from a RPZ hit.
+
+.. _setting-yaml-recursor.forward_zones:
+
+``recursor.forward_zones``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Forward Zone`_
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-forward-zones`
+
+Queries for zones listed here will be forwarded to the IP address listed. i.e.
+
+.. code-block:: yaml
+
+ recursor:
+    forward-zones:
+      - zone: example.org
+        forwarders:
+        - 203.0.113.210
+      - zone: powerdns.com
+        forwarders:
+        - 2001:DB8::BEEF:5
+
+Multiple IP addresses can be specified and port numbers other than 53 can be configured:
+
+.. code-block:: yaml
+
+  recursor:
+    forward-zones:
+    - zone: example.org
+      forwarders:
+      - 203.0.113.210:5300
+      - 127.0.0.1
+    - zone: powerdns.com
+      forwarders:
+      - 127.0.0.1
+      - 198.51.100.10:530
+      - '[2001:DB8::1:3]:5300'
+
+Forwarded queries have the ``recursion desired (RD)`` bit set to ``0``, meaning that this setting is intended to forward queries to authoritative servers.
+If an ``NS`` record set for a subzone of the forwarded zone is learned, that record set will be used to determine addresses for name servers of the subzone.
+This allows e.g. a forward to a local authoritative server holding a copy of the root zone, delegations received from that server will work.
+
+**IMPORTANT**: When using DNSSEC validation (which is default), forwards to non-delegated (e.g. internal) zones that have a DNSSEC signed parent zone will validate as Bogus.
+To prevent this, add a Negative Trust Anchor (NTA) for this zone in the :ref:`setting-yaml-recursor.lua_config_file` with ``addNTA('your.zone', 'A comment')``.
+If this forwarded zone is signed, instead of adding NTA, add the DS record to the :ref:`setting-yaml-recursor.lua_config_file`.
+See the :doc:`dnssec` information.
+
+.. _setting-yaml-recursor.forward_zones_file:
+
+``recursor.forward_zones_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.0.0
+
+  (Old style settings only) Comments are allowed, everything behind ``#`` is ignored.
+.. versionchanged:: 4.6.0
+
+  (Old style settings only) Zones prefixed with a ``^`` are added to the :ref:`setting-allow-notify-for` list. Both prefix characters can be used if desired, in any order.
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-forward-zones-file`
+
+Same as :ref:`setting-yaml-recursor.forward_zones`, parsed from a file as a sequence of `ZoneForward`.
+
+.. code-block:: yaml
+
+  - zone: example1.com
+    forwarders:
+    - 127.0.0.1
+    - 127.0.0.1:5353
+    - '[::1]53'
+  - zone: example2.com
+    forwarders:
+    - ::1
+    recurse: true
+    notify_allowed: true
+
+The DNSSEC notes from :ref:`setting-yaml-recursor.forward_zones` apply here as well.
+
+.. _setting-yaml-recursor.forward_zones_recurse:
+
+``recursor.forward_zones_recurse``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of `Forward Zone`_
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-forward-zones-recurse`
+
+Like regular :ref:`setting-yaml-recursor.forward_zones`, but forwarded queries have the ``recursion desired (RD)`` bit set to ``1``, meaning that this setting is intended to forward queries to other recursive servers.
+In contrast to regular forwarding, the rule that delegations of the forwarded subzones are respected is not active.
+This is because we rely on the forwarder to resolve the query fully.
+
+See :ref:`setting-yaml-recursor.forward_zones` for additional options (such as supplying multiple recursive servers) and an important note about DNSSEC.
+
+.. _setting-yaml-recursor.hint_file:
+
+``recursor.hint_file``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.6.2
+
+  Introduced the value ``no`` to disable root-hints processing.
+.. versionchanged:: 4.9.0
+
+  Introduced the value ``no-refresh`` to disable both root-hints processing and periodic refresh of the cached root `NS` records.
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-hint-file`
+
+If set, the root-hints are read from this file. If empty, the default built-in root hints are used.
+
+In some special cases, processing the root hints is not needed, for example when forwarding all queries to another recursor.
+For these special cases, it is possible to disable the processing of root hints by setting the value to ``no`` or ``no-refresh``.
+See :ref:`handling-of-root-hints` for more information on root hints handling.
+
+.. _setting-yaml-recursor.ignore_unknown_settings:
+
+``recursor.ignore_unknown_settings``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Sequence of strings
+-  Default: ``[]``
+
+- Old style setting: :ref:`setting-ignore-unknown-settings`
+
+Names of settings to be ignored while parsing configuration files, if the setting
+name is unknown to PowerDNS.
+
+Useful during upgrade testing.
+
+.. _setting-yaml-recursor.include_dir:
+
+``recursor.include_dir``
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-include-dir`
+
+Directory to scan for additional config files. All files that end with .conf are loaded in order using ``POSIX`` as locale.
+
+.. _setting-yaml-recursor.latency_statistic_size:
+
+``recursor.latency_statistic_size``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``10000``
+
+- Old style setting: :ref:`setting-latency-statistic-size`
+
+Indication of how many queries will be averaged to get the average latency reported by the 'qa-latency' metric.
+
+.. _setting-yaml-recursor.lua_config_file:
+
+``recursor.lua_config_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-lua-config-file`
+
+If set, and Lua support is compiled in, this will load an additional configuration file for newer features and more complicated setups.
+See :doc:`lua-config/index` for the options that can be set in this file.
+
+.. _setting-yaml-recursor.lua_dns_script:
+
+``recursor.lua_dns_script``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-lua-dns-script`
+
+Path to a lua file to manipulate the Recursor's answers. See :doc:`lua-scripting/index` for more information.
+
+.. _setting-yaml-recursor.lua_maintenance_interval:
+
+``recursor.lua_maintenance_interval``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  Integer
+-  Default: ``1``
+
+- Old style setting: :ref:`setting-lua-maintenance-interval`
+
+The interval between calls to the Lua user defined `maintenance()` function in seconds.
+See :ref:`hooks-maintenance-callback`
+
+.. _setting-yaml-recursor.max_generate_steps:
+
+``recursor.max_generate_steps``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.3.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-max-generate-steps`
+
+Maximum number of steps for a '$GENERATE' directive when parsing a
+zone file. This is a protection measure to prevent consuming a lot of
+CPU and memory when untrusted zones are loaded. Default to 0 which
+means unlimited.
+
+.. _setting-yaml-recursor.max_include_depth:
+
+``recursor.max_include_depth``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Integer
+-  Default: ``20``
+
+- Old style setting: :ref:`setting-max-include-depth`
+
+Maximum number of nested ``$INCLUDE`` directives while processing a zone file.
+Zero mean no ``$INCLUDE`` directives will be accepted.
+
+.. _setting-yaml-recursor.max_mthreads:
+
+``recursor.max_mthreads``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``2048``
+
+- Old style setting: :ref:`setting-max-mthreads`
+
+Maximum number of simultaneous MTasker threads.
+
+.. _setting-yaml-recursor.max_ns_address_qperq:
+
+``recursor.max_ns_address_qperq``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.1.16
+.. versionadded:: 4.2.2
+.. versionadded:: 4.3.1
+
+-  Integer
+-  Default: ``10``
+
+- Old style setting: :ref:`setting-max-ns-address-qperq`
+
+The maximum number of outgoing queries with empty replies for
+resolving nameserver names to addresses we allow during the resolution
+of a single client query. If IPv6 is enabled, an A and a AAAA query
+for a name counts as 1. If a zone publishes more than this number of
+NS records, the limit is further reduced for that zone by lowering
+it by the number of NS records found above the
+:ref:`setting-yaml-recursor.max_ns_address_qperq` value. The limit wil not be reduced to a
+number lower than 5.
+
+.. _setting-yaml-recursor.max_ns_per_resolve:
+
+``recursor.max_ns_per_resolve``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.8.0
+.. versionadded:: 4.7.3
+.. versionadded:: 4.6.4
+.. versionadded:: 4.5.11
+
+-  Integer
+-  Default: ``13``
+
+- Old style setting: :ref:`setting-max-ns-per-resolve`
+
+The maximum number of NS records that will be considered to select a nameserver to contact to resolve a name.
+If a zone has more than :ref:`setting-yaml-recursor.max_ns_per_resolve` NS records, a random sample of this size will be used.
+If :ref:`setting-yaml-recursor.max_ns_per_resolve` is zero, no limit applies.
+
+.. _setting-yaml-recursor.max_qperq:
+
+``recursor.max_qperq``
+^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``60``
+
+- Old style setting: :ref:`setting-max-qperq`
+
+The maximum number of outgoing queries that will be sent out during the resolution of a single client query.
+This is used to limit endlessly chasing CNAME redirections.
+If qname-minimization is enabled, the number will be forced to be 100
+at a minimum to allow for the extra queries qname-minimization generates when the cache is empty.
+
+.. _setting-yaml-recursor.max_recursion_depth:
+
+``recursor.max_recursion_depth``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.1.0
+
+  Before 4.1.0, this settings was unlimited.
+.. versionchanged:: 4.9.0
+
+  Before 4.9.0 this setting's default was 40 and the limit on ``CNAME`` chains (fixed at 16) acted as a bound on he recursion depth.
+
+-  Integer
+-  Default: ``16``
+
+- Old style setting: :ref:`setting-max-recursion-depth`
+
+Total maximum number of internal recursion calls the server may use to answer a single query.
+0 means unlimited.
+The value of :ref:`setting-yaml-recursor.stack_size` should be increased together with this one to prevent the stack from overflowing.
+If :ref:`setting-yaml-recursor.qname_minimization` is enabled, the fallback code in case of a failing resolve is allowed an additional `max-recursion-depth/2`.
+
+.. _setting-yaml-recursor.max_total_msec:
+
+``recursor.max_total_msec``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``7000``
+
+- Old style setting: :ref:`setting-max-total-msec`
+
+Total maximum number of milliseconds of wallclock time the server may use to answer a single query.
+0 means unlimited.
+
+.. _setting-yaml-recursor.minimum_ttl_override:
+
+``recursor.minimum_ttl_override``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.5.0
+
+  Old versions used default 0.
+
+-  Integer
+-  Default: ``1``
+
+- Old style setting: :ref:`setting-minimum-ttl-override`
+
+This setting artificially raises all TTLs to be at least this long.
+Setting this to a value greater than 1 technically is an RFC violation, but might improve performance a lot.
+Using a value of 0 impacts performance of TTL 0 records greatly, since it forces the recursor to contact
+authoritative servers each time a client requests them.
+Can be set at runtime using ``rec_control set-minimum-ttl 3600``.
+
+.. _setting-yaml-recursor.non_resolving_ns_max_fails:
+
+``recursor.non_resolving_ns_max_fails``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Integer
+-  Default: ``5``
+
+- Old style setting: :ref:`setting-non-resolving-ns-max-fails`
+
+Number of failed address resolves of a nameserver name to start throttling it, 0 is disabled.
+Nameservers matching :ref:`setting-yaml-outgoing.dont_throttle_names` will not be throttled.
+
+.. _setting-yaml-recursor.non_resolving_ns_throttle_time:
+
+``recursor.non_resolving_ns_throttle_time``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Integer
+-  Default: ``60``
+
+- Old style setting: :ref:`setting-non-resolving-ns-throttle-time`
+
+Number of seconds to throttle a nameserver with a name failing to resolve.
+
+.. _setting-yaml-recursor.nothing_below_nxdomain:
+
+``recursor.nothing_below_nxdomain``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.3.0
+
+-  String
+-  Default: ``dnssec``
+
+- Old style setting: :ref:`setting-nothing-below-nxdomain`
+
+- One of ``no``, ``dnssec``, ``yes``.
+
+The type of :rfc:`8020` handling using cached NXDOMAIN responses.
+This RFC specifies that NXDOMAIN means that the DNS tree under the denied name MUST be empty.
+When an NXDOMAIN exists in the cache for a shorter name than the qname, no lookup is done and an NXDOMAIN is sent to the client.
+
+For instance, when ``foo.example.net`` is negatively cached, any query
+matching ``*.foo.example.net`` will be answered with NXDOMAIN directly
+without consulting authoritative servers.
+
+``no``
+  No :rfc:`8020` processing is done.
+
+``dnssec``
+  :rfc:`8020` processing is only done using cached NXDOMAIN records that are
+  DNSSEC validated.
+
+``yes``
+  :rfc:`8020` processing is done using any non-Bogus NXDOMAIN record
+  available in the cache.
+
+.. _setting-yaml-recursor.public_suffix_list_file:
+
+``recursor.public_suffix_list_file``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-public-suffix-list-file`
+
+Path to the Public Suffix List file, if any. If set, PowerDNS will try to load the Public Suffix List from this file instead of using the built-in list. The PSL is used to group the queries by relevant domain names when displaying the top queries.
+
+.. _setting-yaml-recursor.qname_minimization:
+
+``recursor.qname_minimization``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.3.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-qname-minimization`
+
+Enable Query Name Minimization. This implements a relaxed form of Query Name Mimimization as
+described in :rfc:`7816`.
+
+.. _setting-yaml-recursor.rng:
+
+``recursor.rng``
+^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``auto``
+
+- Old style setting: :ref:`setting-rng`
+
+- String
+- Default: auto
+
+Specify which random number generator to use. Permissible choices are
+ - auto - choose automatically
+ - sodium - Use libsodium ``randombytes_uniform``
+ - openssl - Use libcrypto ``RAND_bytes``
+ - getrandom - Use libc getrandom, falls back to urandom if it does not really work
+ - arc4random - Use BSD ``arc4random_uniform``
+ - urandom - Use ``/dev/urandom``
+ - kiss - Use simple settable deterministic RNG. **FOR TESTING PURPOSES ONLY!**
+
+.. _setting-yaml-recursor.root_nx_trust:
+
+``recursor.root_nx_trust``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.0.0
+
+  Default is ``yes`` now, was ``no`` before 4.0.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-root-nx-trust`
+
+If set, an NXDOMAIN from the root-servers will serve as a blanket NXDOMAIN for the entire TLD the query belonged to.
+The effect of this is far fewer queries to the root-servers.
+
+.. _setting-yaml-recursor.save_parent_ns_set:
+
+``recursor.save_parent_ns_set``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.7.0
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-save-parent-ns-set`
+
+If set, a parent (non-authoritative) ``NS`` set is saved if it contains more entries than a newly encountered child (authoritative) ``NS`` set for the same domain.
+The saved parent ``NS`` set is tried if resolution using the child ``NS`` set fails.
+
+.. _setting-yaml-recursor.security_poll_suffix:
+
+``recursor.security_poll_suffix``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``secpoll.powerdns.com.``
+
+- Old style setting: :ref:`setting-security-poll-suffix`
+
+Domain name from which to query security update notifications.
+Setting this to an empty string disables secpoll.
+
+.. _setting-yaml-recursor.serve_rfc1918:
+
+``recursor.serve_rfc1918``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-serve-rfc1918`
+
+This makes the server authoritatively aware of: ``10.in-addr.arpa``, ``168.192.in-addr.arpa``, ``16-31.172.in-addr.arpa``, which saves load on the AS112 servers.
+Individual parts of these zones can still be loaded or forwarded.
+
+.. _setting-yaml-recursor.serve_stale_extensions:
+
+``recursor.serve_stale_extensions``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.8.0
+
+-  Integer
+-  Default: ``0``
+
+- Old style setting: :ref:`setting-serve-stale-extensions`
+
+Maximum number of times an expired record's TTL is extended by 30s when serving stale.
+Extension only occurs if a record cannot be refreshed.
+A value of 0 means the ``Serve Stale`` mechanism is not used.
+To allow records becoming stale to be served for an hour, use a value of 120.
+See :ref:`serve-stale` for a description of the Serve Stale mechanism.
+
+.. _setting-yaml-recursor.server_down_max_fails:
+
+``recursor.server_down_max_fails``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``64``
+
+- Old style setting: :ref:`setting-server-down-max-fails`
+
+If a server has not responded in any way this many times in a row, no longer send it any queries for :ref:`setting-yaml-recursor.server_down_throttle_time` seconds.
+Afterwards, we will try a new packet, and if that also gets no response at all, we again throttle for :ref:`setting-yaml-recursor.server_down_throttle_time` seconds.
+Even a single response packet will drop the block.
+
+.. _setting-yaml-recursor.server_down_throttle_time:
+
+``recursor.server_down_throttle_time``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``60``
+
+- Old style setting: :ref:`setting-server-down-throttle-time`
+
+Throttle a server that has failed to respond :ref:`setting-yaml-recursor.server_down_max_fails` times for this many seconds.
+
+.. _setting-yaml-recursor.server_id:
+
+``recursor.server_id``
+^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``*runtime determined*``
+
+- Old style setting: :ref:`setting-server-id`
+
+The reply given by The PowerDNS recursor to a query for 'id.server' with its hostname, useful for in clusters.
+When a query contains the :rfc:`NSID EDNS0 Option <5001>`, this value is returned in the response as the NSID value.
+
+This setting can be used to override the answer given to these queries.
+Set to 'disabled' to disable NSID and 'id.server' answers.
+
+Query example (where 192.0.2.14 is your server):
+
+.. code-block:: sh
+
+    dig @192.0.2.14 CHAOS TXT id.server.
+    dig @192.0.2.14 example.com IN A +nsid
+
+.. _setting-yaml-recursor.setgid:
+
+``recursor.setgid``
+^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-setgid`
+
+PowerDNS can change its user and group id after binding to its socket.
+Can be used for better :doc:`security <security>`.
+
+.. _setting-yaml-recursor.setuid:
+
+``recursor.setuid``
+^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-setuid`
+
+PowerDNS can change its user and group id after binding to its socket.
+Can be used for better :doc:`security <security>`.
+
+.. _setting-yaml-recursor.socket_dir:
+
+``recursor.socket_dir``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-socket-dir`
+
+Where to store the control socket and pidfile.
+The default depends on ``LOCALSTATEDIR`` or the ``--with-socketdir`` setting when building (usually ``/var/run`` or ``/run``).
+
+When using :ref:`setting-yaml-recursor.chroot` the default becomes ``/``.
+
+.. _setting-yaml-recursor.socket_group:
+
+``recursor.socket_group``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-socket-group`
+
+Group and mode of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+
+.. _setting-yaml-recursor.socket_mode:
+
+``recursor.socket_mode``
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-socket-mode`
+
+Mode of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+
+.. _setting-yaml-recursor.socket_owner:
+
+``recursor.socket_owner``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-socket-owner`
+
+Owner of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+
+.. _setting-yaml-recursor.spoof_nearmiss_max:
+
+``recursor.spoof_nearmiss_max``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.5.0
+
+  Older versions used 20 as the default value.
+
+-  Integer
+-  Default: ``1``
+
+- Old style setting: :ref:`setting-spoof-nearmiss-max`
+
+If set to non-zero, PowerDNS will assume it is being spoofed after seeing this many answers with the wrong id.
+
+.. _setting-yaml-recursor.stack_cache_size:
+
+``recursor.stack_cache_size``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.9.0
+
+-  Integer
+-  Default: ``100``
+
+- Old style setting: :ref:`setting-stack-cache-size`
+
+Maximum number of mthread stacks that can be cached for later reuse, per thread. Caching these stacks reduces the CPU load at the cost of a slightly higher memory usage, each cached stack consuming `stack-size` bytes of memory.
+It makes no sense to cache more stacks than the value of `max-mthreads`, since there will never be more stacks than that in use at a given time.
+
+.. _setting-yaml-recursor.stack_size:
+
+``recursor.stack_size``
+^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``200000``
+
+- Old style setting: :ref:`setting-stack-size`
+
+Size in bytes of the stack of each mthread.
+
+.. _setting-yaml-recursor.stats_api_disabled_list:
+
+``recursor.stats_api_disabled_list``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*
+
+- Old style setting: :ref:`setting-stats-api-disabled-list`
+
+A sequence of statistic names, that are disabled when retrieving the complete list of statistics via the API for performance reasons.
+These statistics can still be retrieved individually by specifically asking for it.
+
+.. _setting-yaml-recursor.stats_carbon_disabled_list:
+
+``recursor.stats_carbon_disabled_list``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*
+
+- Old style setting: :ref:`setting-stats-carbon-disabled-list`
+
+A sequence of statistic names, that are prevented from being exported via carbon for performance reasons.
+
+.. _setting-yaml-recursor.stats_rec_control_disabled_list:
+
+``recursor.stats_rec_control_disabled_list``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*
+
+- Old style setting: :ref:`setting-stats-rec-control-disabled-list`
+
+A sequence of statistic names, that are disabled when retrieving the complete list of statistics via `rec_control get-all`, for performance reasons.
+These statistics can still be retrieved individually.
+
+.. _setting-yaml-recursor.stats_ringbuffer_entries:
+
+``recursor.stats_ringbuffer_entries``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``10000``
+
+- Old style setting: :ref:`setting-stats-ringbuffer-entries`
+
+Number of entries in the remotes ringbuffer, which keeps statistics on who is querying your server.
+Can be read out using ``rec_control top-remotes``.
+
+.. _setting-yaml-recursor.stats_snmp_disabled_list:
+
+``recursor.stats_snmp_disabled_list``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  Sequence of strings
+-  Default: cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*
+
+- Old style setting: :ref:`setting-stats-snmp-disabled-list`
+
+A sequence of statistic names, that are prevented from being exported via SNMP, for performance reasons.
+
+.. _setting-yaml-recursor.threads:
+
+``recursor.threads``
+^^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``2``
+
+- Old style setting: :ref:`setting-threads`
+
+Spawn this number of threads on startup.
+
+.. _setting-yaml-recursor.version_string:
+
+``recursor.version_string``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``*runtime determined*``
+
+- Old style setting: :ref:`setting-version-string`
+
+By default, PowerDNS replies to the 'version.bind' query with its version number.
+Security conscious users may wish to override the reply PowerDNS issues.
+
+.. _setting-yaml-recursor.write_pid:
+
+``recursor.write_pid``
+^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``true``
+
+- Old style setting: :ref:`setting-write-pid`
+
+If a PID file should be written to :ref:`setting-yaml-recursor.socket_dir`
+
+.. _setting-yaml-snmp.agent:
+
+``snmp.agent``
+^^^^^^^^^^^^^^
+.. versionadded:: 4.1.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-snmp-agent`
+
+If set to true and PowerDNS has been compiled with SNMP support, it will register as an SNMP agent to provide statistics and be able to send traps.
+
+.. _setting-yaml-snmp.daemon_socket:
+
+``snmp.daemon_socket``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.5.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-snmp-daemon-socket`
+
+If not empty and ``snmp-agent`` is set to true, indicates how PowerDNS should contact the SNMP daemon to register as an SNMP agent.
+
+.. _setting-yaml-webservice.address:
+
+``webservice.address``
+^^^^^^^^^^^^^^^^^^^^^^
+
+-  String
+-  Default: ``127.0.0.1``
+
+- Old style setting: :ref:`setting-webserver-address`
+
+IP address for the webserver to listen on.
+
+.. _setting-yaml-webservice.allow_from:
+
+``webservice.allow_from``
+^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.1.0
+
+  Default is now 127.0.0.1,::1, was 0.0.0.0/0,::/0 before.
+
+-  Sequence of `Subnet`_ (IP addresses or subnets, negation supported)
+-  Default: ``[127.0.0.1, ::1]``
+
+- Old style setting: :ref:`setting-webserver-allow-from`
+
+These IPs and subnets are allowed to access the webserver. Note that
+specifying an IP address without a netmask uses an implicit netmask
+of /32 or /128.
+
+.. _setting-yaml-webservice.api_dir:
+
+``webservice.api_dir``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.0.0
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-api-config-dir`
+
+Directory where the REST API stores its configuration and zones.
+For configuration updates to work, :ref:`setting-yaml-recursor.include_dir` should have the same value.
+
+.. _setting-yaml-webservice.api_key:
+
+``webservice.api_key``
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.0.0
+.. versionchanged:: 4.6.0
+
+  This setting now accepts a hashed and salted version.
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-api-key`
+
+Static pre-shared authentication key for access to the REST API. Since 4.6.0 the key can be hashed and salted using ``rec_control hash-password`` instead of being stored in the configuration in plaintext, but the plaintext version is still supported.
+
+.. _setting-yaml-webservice.hash_plaintext_credentials:
+
+``webservice.hash_plaintext_credentials``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.6.0
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-webserver-hash-plaintext-credentials`
+
+Whether passwords and API keys supplied in the configuration as plaintext should be hashed during startup, to prevent the plaintext versions from staying in memory. Doing so increases significantly the cost of verifying credentials and is thus disabled by default.
+Note that this option only applies to credentials stored in the configuration as plaintext, but hashed credentials are supported without enabling this option.
+
+.. _setting-yaml-webservice.loglevel:
+
+``webservice.loglevel``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.2.0
+
+-  String
+-  Default: ``normal``
+
+- Old style setting: :ref:`setting-webserver-loglevel`
+
+One of ``one``, ``normal``, ``detailed``.
+The amount of logging the webserver must do. 'none' means no useful webserver information will be logged.
+When set to 'normal', the webserver will log a line per request that should be familiar::
+
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 'GET /api/v1/servers/localhost/bla HTTP/1.1' 404 196
+
+When set to 'detailed', all information about the request and response are logged::
+
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e Request Details:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Headers:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   accept-encoding: gzip, deflate
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   accept-language: en-US,en;q=0.5
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   connection: keep-alive
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   dnt: 1
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   host: 127.0.0.1:8081
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   upgrade-insecure-requests: 1
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  No body
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e Response details:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Headers:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Connection: close
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Content-Length: 49
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Content-Type: text/html; charset=utf-8
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Server: PowerDNS/0.0.15896.0.gaba8bab3ab
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Full body:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   <!html><title>Not Found</title><h1>Not Found</h1>
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 'GET /api/v1/servers/localhost/bla HTTP/1.1' 404 196
+
+The value between the hooks is a UUID that is generated for each request. This can be used to find all lines related to a single request.
+
+.. note::
+  The webserver logs these line on the NOTICE level. The :ref:`setting-yaml-logging.loglevel` seting must be 5 or higher for these lines to end up in the log.
+
+.. _setting-yaml-webservice.password:
+
+``webservice.password``
+^^^^^^^^^^^^^^^^^^^^^^^
+.. versionchanged:: 4.6.0
+
+  This setting now accepts a hashed and salted version.
+
+-  String
+-  Default: (empty)
+
+- Old style setting: :ref:`setting-webserver-password`
+
+Password required to access the webserver. Since 4.6.0 the password can be hashed and salted using ``rec_control hash-password`` instead of being present in the configuration in plaintext, but the plaintext version is still supported.
+
+.. _setting-yaml-webservice.port:
+
+``webservice.port``
+^^^^^^^^^^^^^^^^^^^
+
+-  Integer
+-  Default: ``8082``
+
+- Old style setting: :ref:`setting-webserver-port`
+
+TCP port where the webserver should listen on.
+
+.. _setting-yaml-webservice.webserver:
+
+``webservice.webserver``
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-webserver`
+
+Start the webserver (for REST API).
+
index 36b8517013d1c79d707dd652fb723cd1b60927d2..10469fb445080a6669b3f087af6201f1aead4fc1 100644 (file)
@@ -39,6 +39,7 @@
 #include "secpoll-recursor.hh"
 #include "logging.hh"
 #include "dnsseckeeper.hh"
+#include "settings/cxxsettings.hh"
 
 #ifdef NOD_ENABLED
 #include "nod.hh"
@@ -67,6 +68,7 @@ string g_programname = "pdns_recursor";
 string g_pidfname;
 RecursorControlChannel g_rcc; // only active in the handler thread
 bool g_regressionTestMode;
+bool g_yamlSettings;
 
 #ifdef NOD_ENABLED
 bool g_nodEnabled;
@@ -754,7 +756,7 @@ static void setupNODThread(Logr::log_t log)
     }
     catch (const PDNSException& e) {
       SLOG(g_log << Logger::Error << "new-domain-history-dir (" << ::arg()["new-domain-history-dir"] << ") is not readable or does not exist" << endl,
-           log->error(Logr::Error, e.reason, "new-domain-history-dir is not readbale or does not exists", "dir", Logging::Loggable(::arg()["new-domain-history-dir"])));
+           log->error(Logr::Error, e.reason, "new-domain-history-dir is not readable or does not exists", "dir", Logging::Loggable(::arg()["new-domain-history-dir"])));
       _exit(1);
     }
     if (!t_nodDBp->init()) {
@@ -854,7 +856,7 @@ static void usr2Handler([[maybe_unused]] int arg)
 {
   g_quiet = !g_quiet;
   SyncRes::setDefaultLogMode(g_quiet ? SyncRes::LogNone : SyncRes::Log);
-  ::arg().set("quiet") = g_quiet ? "" : "no";
+  ::arg().set("quiet") = g_quiet ? "yes" : "no";
 }
 
 static void checkLinuxIPv6Limits([[maybe_unused]] Logr::log_t log)
@@ -1147,28 +1149,40 @@ static std::shared_ptr<NetmaskGroup> parseACL(const std::string& aclFile, const
 {
   auto result = std::make_shared<NetmaskGroup>();
 
-  if (!::arg()[aclFile].empty()) {
-    string line;
-    ifstream ifs(::arg()[aclFile].c_str());
-    if (!ifs) {
-      throw runtime_error("Could not open '" + ::arg()[aclFile] + "': " + stringerror());
-    }
+  const string file = ::arg()[aclFile];
 
-    while (getline(ifs, line)) {
-      auto pos = line.find('#');
-      if (pos != string::npos) {
-        line.resize(pos);
+  if (!file.empty()) {
+    if (boost::ends_with(file, ".yml")) {
+      ::rust::vec<::rust::string> vec;
+      pdns::settings::rec::readYamlAllowFromFile(file, vec, log);
+      for (const auto& subnet : vec) {
+        result->addMask(string(subnet));
       }
-      boost::trim(line);
-      if (line.empty()) {
-        continue;
+    }
+    else {
+      string line;
+      ifstream ifs(file);
+      if (!ifs) {
+        int err = errno;
+        throw runtime_error("Could not open '" + file + "': " + stringerror(err));
       }
 
-      result->addMask(line);
+      while (getline(ifs, line)) {
+        auto pos = line.find('#');
+        if (pos != string::npos) {
+          line.resize(pos);
+        }
+        boost::trim(line);
+        if (line.empty()) {
+          continue;
+        }
+
+        result->addMask(line);
+      }
     }
-    SLOG(g_log << Logger::Info << "Done parsing " << result->size() << " " << aclSetting << " ranges from file '" << ::arg()[aclFile] << "' - overriding '" << aclSetting << "' setting" << endl,
+    SLOG(g_log << Logger::Info << "Done parsing " << result->size() << " " << aclSetting << " ranges from file '" << file << "' - overriding '" << aclSetting << "' setting" << endl,
          log->info(Logr::Info, "Done parsing ranges from file, will override setting", "setting", Logging::Loggable(aclSetting),
-                   "number", Logging::Loggable(result->size()), "file", Logging::Loggable(::arg()[aclFile])));
+                   "number", Logging::Loggable(result->size()), "file", Logging::Loggable(file)));
   }
   else if (!::arg()[aclSetting].empty()) {
     vector<string> ips;
@@ -1220,51 +1234,75 @@ void parseACLs()
   static bool l_initialized;
 
   if (l_initialized) { // only reload configuration file on second call
-    string configName = ::arg()["config-dir"] + "/recursor.conf";
+
+    string configName = ::arg()["config-dir"] + "/recursor";
     if (!::arg()["config-name"].empty()) {
-      configName = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"] + ".conf";
+      configName = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"];
     }
     cleanSlashes(configName);
 
-    if (!::arg().preParseFile(configName.c_str(), "allow-from-file")) {
-      throw runtime_error("Unable to re-parse configuration file '" + configName + "'");
+    if (g_yamlSettings) {
+      configName += ".yml";
+      string msg;
+      pdns::rust::settings::rec::Recursorsettings settings;
+      // XXX Does ::arg()["include-dir"] have the right value, i.e. potentially overriden by command line?
+      auto yamlstatus = pdns::settings::rec::readYamlSettings(configName, ::arg()["include-dir"], settings, msg, log);
+
+      switch (yamlstatus) {
+      case pdns::settings::rec::YamlSettingsStatus::CannotOpen:
+        throw runtime_error("Unable to open '" + configName + "': " + msg);
+        break;
+      case pdns::settings::rec::YamlSettingsStatus::PresentButFailed:
+        throw runtime_error("Error processing '" + configName + "': " + msg);
+        break;
+      case pdns::settings::rec::YamlSettingsStatus::OK:
+        // Does *not* set include-dir
+        pdns::settings::rec::setArgsForACLRelatedSettings(settings);
+        break;
+      }
     }
-    ::arg().preParseFile(configName.c_str(), "allow-from", LOCAL_NETS);
+    else {
+      configName += ".conf";
+      if (!::arg().preParseFile(configName, "allow-from-file")) {
+        throw runtime_error("Unable to re-parse configuration file '" + configName + "'");
+      }
+      ::arg().preParseFile(configName, "allow-from", LOCAL_NETS);
 
-    if (!::arg().preParseFile(configName.c_str(), "allow-notify-from-file")) {
-      throw runtime_error("Unable to re-parse configuration file '" + configName + "'");
-    }
-    ::arg().preParseFile(configName.c_str(), "allow-notify-from");
+      if (!::arg().preParseFile(configName, "allow-notify-from-file")) {
+        throw runtime_error("Unable to re-parse configuration file '" + configName + "'");
+      }
+      ::arg().preParseFile(configName, "allow-notify-from");
 
-    ::arg().preParseFile(configName.c_str(), "include-dir");
-    ::arg().preParse(g_argc, g_argv, "include-dir");
+      ::arg().preParseFile(configName, "include-dir");
+      ::arg().preParse(g_argc, g_argv, "include-dir");
 
-    // then process includes
-    std::vector<std::string> extraConfigs;
-    ::arg().gatherIncludes(extraConfigs);
+      // then process includes
+      std::vector<std::string> extraConfigs;
+      ::arg().gatherIncludes(::arg()["include-dir"], ".conf", extraConfigs);
 
-    for (const std::string& fileName : extraConfigs) {
-      if (!::arg().preParseFile(fileName.c_str(), "allow-from-file", ::arg()["allow-from-file"])) {
-        throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
-      }
-      if (!::arg().preParseFile(fileName.c_str(), "allow-from", ::arg()["allow-from"])) {
-        throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
-      }
+      for (const std::string& fileName : extraConfigs) {
+        if (!::arg().preParseFile(fileName, "allow-from-file", ::arg()["allow-from-file"])) {
+          throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
+        }
+        if (!::arg().preParseFile(fileName, "allow-from", ::arg()["allow-from"])) {
+          throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
+        }
 
-      if (!::arg().preParseFile(fileName.c_str(), "allow-notify-from-file", ::arg()["allow-notify-from-file"])) {
-        throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
-      }
-      if (!::arg().preParseFile(fileName.c_str(), "allow-notify-from", ::arg()["allow-notify-from"])) {
-        throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
+        if (!::arg().preParseFile(fileName, "allow-notify-from-file", ::arg()["allow-notify-from-file"])) {
+          throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
+        }
+        if (!::arg().preParseFile(fileName, "allow-notify-from", ::arg()["allow-notify-from"])) {
+          throw runtime_error("Unable to re-parse configuration file include '" + fileName + "'");
+        }
       }
     }
-
-    ::arg().preParse(g_argc, g_argv, "allow-from-file");
-    ::arg().preParse(g_argc, g_argv, "allow-from");
-
-    ::arg().preParse(g_argc, g_argv, "allow-notify-from-file");
-    ::arg().preParse(g_argc, g_argv, "allow-notify-from");
   }
+  // Process command line args potentially overriding settings read from file
+  ::arg().preParse(g_argc, g_argv, "allow-from-file");
+  ::arg().preParse(g_argc, g_argv, "allow-from");
+
+  ::arg().preParse(g_argc, g_argv, "allow-notify-from-file");
+  ::arg().preParse(g_argc, g_argv, "allow-notify-from");
 
   auto allowFrom = parseACL("allow-from-file", "allow-from", log);
 
@@ -1556,7 +1594,7 @@ static void initDontQuery(Logr::log_t log)
   }
 }
 
-static int initSyncRes(Logr::log_t log, const std::optional<std::string>& myHostname)
+static int initSyncRes(Logr::log_t log)
 {
   SyncRes::s_minimumTTL = ::arg().asNum("minimum-ttl-override");
   SyncRes::s_minimumECSTTL = ::arg().asNum("ecs-minimum-ttl-override");
@@ -1605,11 +1643,6 @@ static int initSyncRes(Logr::log_t log, const std::optional<std::string>& myHost
     checkFastOpenSysctl(true, log);
     checkTFOconnect(log);
   }
-
-  if (SyncRes::s_serverID.empty()) {
-    SyncRes::s_serverID = myHostname.has_value() ? *myHostname : "";
-  }
-
   SyncRes::s_ecsipv4limit = ::arg().asNum("ecs-ipv4-bits");
   SyncRes::s_ecsipv6limit = ::arg().asNum("ecs-ipv6-bits");
   SyncRes::clearECSStats();
@@ -2020,13 +2053,8 @@ static int serviceMain(Logr::log_t log)
     ::arg().set("quiet") = "no";
     g_quiet = false;
   }
-  auto myHostname = getHostname();
-  if (!myHostname.has_value()) {
-    SLOG(g_log << Logger::Warning << "Unable to get the hostname, NSID and id.server values will be empty" << endl,
-         log->info(Logr::Warning, "Unable to get the hostname, NSID and id.server values will be empty"));
-  }
 
-  ret = initSyncRes(log, myHostname);
+  ret = initSyncRes(log);
   if (ret != 0) {
     return ret;
   }
@@ -2040,7 +2068,7 @@ static int serviceMain(Logr::log_t log)
   }
   g_networkTimeoutMsec = ::arg().asNum("network-timeout");
 
-  std::tie(g_initialDomainMap, g_initialAllowNotifyFor) = parseZoneConfiguration();
+  std::tie(g_initialDomainMap, g_initialAllowNotifyFor) = parseZoneConfiguration(g_yamlSettings);
 
   g_latencyStatSize = ::arg().asNum("latency-statistic-size");
 
@@ -2137,10 +2165,6 @@ static int serviceMain(Logr::log_t log)
   openssl_thread_setup();
   openssl_seed();
 
-  if (::arg()["server-id"].empty()) {
-    ::arg().set("server-id") = myHostname.has_value() ? *myHostname : "";
-  }
-
   gid_t newgid = 0;
   if (!::arg()["setgid"].empty()) {
     newgid = strToGID(::arg()["setgid"]);
@@ -2798,6 +2822,8 @@ static void recursorThread()
   }
 }
 
+#if 0
+// THIS FUNCTION IS REPLACED BY GENERATED CODE IN settings/cxxsettings-generated.cc
 static void initArgs()
 {
 #if HAVE_FIBER_SANITIZER
@@ -2810,21 +2836,21 @@ static void initArgs()
   // This mode forces metrics snap updates and disable root-refresh, to get consistent counters
   ::arg().setSwitch("devonly-regression-test-mode", "internal use only") = "no";
   ::arg().set("soa-minimum-ttl", "Don't change") = "0";
-  ::arg().set("no-shuffle", "Don't change") = "off";
+  ::arg().setSwitch("no-shuffle", "Don't change") = "off";
   ::arg().set("local-port", "port to listen on") = "53";
   ::arg().set("local-address", "IP addresses to listen on, separated by spaces or commas. Also accepts ports.") = "127.0.0.1";
   ::arg().setSwitch("non-local-bind", "Enable binding to non-local addresses by using FREEBIND / BINDANY socket options") = "no";
   ::arg().set("trace", "if we should output heaps of logging. set to 'fail' to only log failing domains") = "off";
   ::arg().set("dnssec", "DNSSEC mode: off/process-no-validate/process (default)/log-fail/validate") = "process";
-  ::arg().set("dnssec-log-bogus", "Log DNSSEC bogus validations") = "no";
+  ::arg().setSwitch("dnssec-log-bogus", "Log DNSSEC bogus validations") = "no";
   ::arg().set("signature-inception-skew", "Allow the signature inception to be off by this number of seconds") = "60";
   ::arg().set("dnssec-disabled-algorithms", "List of DNSSEC algorithm numbers that are considered unsupported") = "";
-  ::arg().set("daemon", "Operate as a daemon") = "no";
+  ::arg().setSwitch("daemon", "Operate as a daemon") = "no";
   ::arg().setSwitch("write-pid", "Write a PID file") = "yes";
   ::arg().set("loglevel", "Amount of logging. Higher is more. Do not set below 3") = "6";
-  ::arg().set("disable-syslog", "Disable logging to syslog, useful when running inside a supervisor that logs stdout") = "no";
-  ::arg().set("log-timestamp", "Print timestamps in log lines, useful to disable when running with a tool that timestamps stdout already") = "yes";
-  ::arg().set("log-common-errors", "If we should log rather common errors") = "no";
+  ::arg().setSwitch("disable-syslog", "Disable logging to syslog, useful when running inside a supervisor that logs stdout") = "no";
+  ::arg().setSwitch("log-timestamp", "Print timestamps in log lines, useful to disable when running with a tool that timestamps stdout already") = "yes";
+  ::arg().setSwitch("log-common-errors", "If we should log rather common errors") = "no";
   ::arg().set("chroot", "switch to chroot jail") = "";
   ::arg().set("setgid", "If set, change group id to this gid for more security"
 #ifdef HAVE_SYSTEMD
@@ -2860,7 +2886,7 @@ static void initArgs()
   ::arg().set("carbon-instance", "If set overwrites the instance name default") = "recursor";
 
   ::arg().set("statistics-interval", "Number of seconds between printing of recursor statistics, 0 to disable") = "1800";
-  ::arg().set("quiet", "Suppress logging of questions and answers") = "";
+  ::arg().setSwitch("quiet", "Suppress logging of questions and answers") = "yes";
   ::arg().set("logging-facility", "Facility to log messages as. 0 corresponds to local0") = "";
   ::arg().set("config-dir", "Location of configuration directory (recursor.conf)") = SYSCONFDIR;
   ::arg().set("socket-owner", "Owner of socket") = "";
@@ -2914,7 +2940,7 @@ static void initArgs()
   ::arg().set("max-tcp-per-client", "If set, maximum number of TCP sessions per client (IP address)") = "0";
   ::arg().set("max-tcp-queries-per-connection", "If set, maximum number of TCP queries in a TCP connection") = "0";
   ::arg().set("spoof-nearmiss-max", "If non-zero, assume spoofing after this many near misses") = "1";
-  ::arg().set("single-socket", "If set, only use a single socket for outgoing queries") = "off";
+  ::arg().setSwitch("single-socket", "If set, only use a single socket for outgoing queries") = "off";
   ::arg().set("auth-zones", "Zones for which we have authoritative data, comma separated domain=file pairs ") = "";
   ::arg().set("lua-config-file", "More powerful configuration options") = "";
   ::arg().setSwitch("allow-trust-anchor-query", "Allow queries for trustanchor.server CH TXT and negativetrustanchor.server CH TXT") = "no";
@@ -2922,10 +2948,10 @@ static void initArgs()
   ::arg().set("forward-zones", "Zones for which we forward queries, comma separated domain=ip pairs") = "";
   ::arg().set("forward-zones-recurse", "Zones for which we forward queries with recursion bit, comma separated domain=ip pairs") = "";
   ::arg().set("forward-zones-file", "File with (+)domain=ip pairs for forwarding") = "";
-  ::arg().set("export-etc-hosts", "If we should serve up contents from /etc/hosts") = "off";
+  ::arg().setSwitch("export-etc-hosts", "If we should serve up contents from /etc/hosts") = "off";
   ::arg().set("export-etc-hosts-search-suffix", "Also serve up the contents of /etc/hosts with this suffix") = "";
   ::arg().set("etc-hosts-file", "Path to 'hosts' file") = "/etc/hosts";
-  ::arg().set("serve-rfc1918", "If we should be authoritative for RFC 1918 private IP space") = "yes";
+  ::arg().setSwitch("serve-rfc1918", "If we should be authoritative for RFC 1918 private IP space") = "yes";
   ::arg().set("lua-dns-script", "Filename containing an optional 'lua' script that will be used to modify dns answers") = "";
   ::arg().set("lua-maintenance-interval", "Number of seconds between calls to the lua user defined maintenance() function") = "1";
   ::arg().set("latency-statistic-size", "Number of latency values to calculate the qa-latency average") = "10000";
@@ -2992,7 +3018,7 @@ static void initArgs()
   ::arg().set("stats-snmp-disabled-list", "List of statistics that are prevented from being exported via SNMP") = defaultDisabledStats;
 
   ::arg().set("tcp-fast-open", "Enable TCP Fast Open support on the listening sockets, using the supplied numerical value as the queue size") = "0";
-  ::arg().set("tcp-fast-open-connect", "Enable TCP Fast Open support on outgoing sockets") = "no";
+  ::arg().setSwitch("tcp-fast-open-connect", "Enable TCP Fast Open support on outgoing sockets") = "no";
   ::arg().set("nsec3-max-iterations", "Maximum number of iterations allowed for an NSEC3 record") = "150";
 
   ::arg().set("cpu-map", "Thread to CPU mapping, space separated thread-id=cpu1,cpu2..cpuN pairs") = "";
@@ -3012,7 +3038,7 @@ static void initArgs()
   ::arg().set("distribution-load-factor", "The load factor used when PowerDNS is distributing queries to worker threads") = "0.0";
 
   ::arg().setSwitch("qname-minimization", "Use Query Name Minimization") = "yes";
-  ::arg().setSwitch("nothing-below-nxdomain", "When an NXDOMAIN exists in cache for a name with fewer labels than the qname, send NXDOMAIN without doing a lookup (see RFC 8020)") = "dnssec";
+  ::arg().set("nothing-below-nxdomain", "When an NXDOMAIN exists in cache for a name with fewer labels than the qname, send NXDOMAIN without doing a lookup (see RFC 8020)") = "dnssec";
   ::arg().set("max-generate-steps", "Maximum number of $GENERATE steps when loading a zone from a file") = "0";
   ::arg().set("max-include-depth", "Maximum nested $INCLUDE depth when loading a zone from a file") = "20";
 
@@ -3025,16 +3051,16 @@ static void initArgs()
   ::arg().set("x-dnssec-names", "Collect DNSSEC statistics for names or suffixes in this list in separate x-dnssec counters") = "";
 
 #ifdef NOD_ENABLED
-  ::arg().set("new-domain-tracking", "Track newly observed domains (i.e. never seen before).") = "no";
-  ::arg().set("new-domain-log", "Log newly observed domains.") = "yes";
+  ::arg().setSwitch("new-domain-tracking", "Track newly observed domains (i.e. never seen before).") = "no";
+  ::arg().setSwitch("new-domain-log", "Log newly observed domains.") = "yes";
   ::arg().set("new-domain-lookup", "Perform a DNS lookup newly observed domains as a subdomain of the configured domain") = "";
   ::arg().set("new-domain-history-dir", "Persist new domain tracking data here to persist between restarts") = string(NODCACHEDIR) + "/nod";
   ::arg().set("new-domain-whitelist", "List of domains (and implicitly all subdomains) which will never be considered a new domain (deprecated)") = "";
   ::arg().set("new-domain-ignore-list", "List of domains (and implicitly all subdomains) which will never be considered a new domain") = "";
   ::arg().set("new-domain-db-size", "Size of the DB used to track new domains in terms of number of cells. Defaults to 67108864") = "67108864";
   ::arg().set("new-domain-pb-tag", "If protobuf is configured, the tag to use for messages containing newly observed domains. Defaults to 'pdns-nod'") = "pdns-nod";
-  ::arg().set("unique-response-tracking", "Track unique responses (tuple of query name, type and RR).") = "no";
-  ::arg().set("unique-response-log", "Log unique responses") = "yes";
+  ::arg().setSwitch("unique-response-tracking", "Track unique responses (tuple of query name, type and RR).") = "no";
+  ::arg().setSwitch("unique-response-log", "Log unique responses") = "yes";
   ::arg().set("unique-response-history-dir", "Persist unique response tracking data here to persist between restarts") = string(NODCACHEDIR) + "/udr";
   ::arg().set("unique-response-db-size", "Size of the DB used to track unique responses in terms of number of cells. Defaults to 67108864") = "67108864";
   ::arg().set("unique-response-pb-tag", "If protobuf is configured, the tag to use for messages containing unique DNS responses. Defaults to 'pdns-udr'") = "pdns-udr";
@@ -3069,6 +3095,58 @@ static void initArgs()
   ::arg().setCmd("config", "Output blank configuration. You can use --config=check to test the config file and command line arguments.");
   ::arg().setDefaults();
   g_log.toConsole(Logger::Info);
+
+  if (s_generateConfigTable) {
+    auto file = ofstream("settings/table.py");
+    const std::map<std::string, std::string> overrideMap = {
+      {"allow-from", "LType.ListSubnets"},
+      {"allow-notify-for", "LType.ListStrings"},
+      {"allow-notify-from", "LType.ListSubnets"},
+      {"auth-zones", "LType.ListStrings"},
+      {"dnssec-disabled-algorithms", "LType.ListStrings"},
+      {"dont-query", "LType.ListSubnets"},
+      {"dont-throttle-names", "LType.ListStrings"},
+      {"dont-throttle-netmasks", "LType.ListSubnets"},
+      {"dot-to-auth-names", "LType.ListStrings"},
+      {"ecs-add-for", "LType.ListSubnets"},
+      {"edbs-padding-from", "LType.ListSubnets"},
+      {"edns-subnet-allow-list", "LType.ListSubnets"},
+      {"forwad-zones", "LType.ListStrings"},
+      {"forwad-zones-recurse", "LType.Strings"},
+      {"local-address", "LType.ListSocketAddresses"},
+      {"new-domain-ignore-list", "LType.ListStrings"},
+      {"query-local-address", "LType.ListSubnets"},
+      {"stats-api-disabled-list", "LType.ListStrings"},
+      {"stats-carbon-disabled-list", "LType.ListStrings"},
+      {"stats-rec-control-disabled-list", "LType.ListStrings"},
+      {"stats-snmp-disabled-list", "LType.ListStrings"},
+      {"webserver-allow-from", "LType.ListSubnets"},
+      {"x-dnssec-names", "LType.ListStrings"},
+    };
+    file << ::arg().table(overrideMap);
+    file.close();
+  }
+}
+#endif
+
+static pair<int, bool> doYamlConfig(Logr::log_t /* startupLog */, int argc, char* argv[]) // NOLINT: Posix API
+{
+  if (!::arg().mustDo("config")) {
+    return {0, false};
+  }
+  const string config = ::arg()["config"];
+  if (config == "diff" || config.empty()) {
+    ::arg().parse(argc, argv);
+    pdns::rust::settings::rec::Recursorsettings settings;
+    pdns::settings::rec::oldStyleSettingsToBridgeStruct(settings);
+    auto yaml = settings.to_yaml_string();
+    cout << yaml << endl;
+  }
+  else if (config == "default") {
+    auto yaml = pdns::settings::rec::defaultsToYaml();
+    cout << yaml << endl;
+  }
+  return {0, true};
 }
 
 static pair<int, bool> doConfig(Logr::log_t startupLog, const string& configname, int argc, char* argv[]) // NOLINT: Posix API
@@ -3077,7 +3155,7 @@ static pair<int, bool> doConfig(Logr::log_t startupLog, const string& configname
     string config = ::arg()["config"];
     if (config == "check") {
       try {
-        if (!::arg().file(configname.c_str())) {
+        if (!::arg().file(configname)) {
           SLOG(g_log << Logger::Warning << "Unable to open configuration file '" << configname << "'" << endl,
                startupLog->error("No such file", "Unable to open configuration file", "config_file", Logging::Loggable(configname)));
           return {1, true};
@@ -3095,7 +3173,7 @@ static pair<int, bool> doConfig(Logr::log_t startupLog, const string& configname
       cout << ::arg().configstring(false, true);
     }
     else if (config == "diff") {
-      if (!::arg().laxFile(configname.c_str())) {
+      if (!::arg().laxFile(configname)) {
         SLOG(g_log << Logger::Warning << "Unable to open configuration file '" << configname << "'" << endl,
              startupLog->error("No such file", "Unable to open configuration file", "config_file", Logging::Loggable(configname)));
         return {1, true};
@@ -3104,7 +3182,7 @@ static pair<int, bool> doConfig(Logr::log_t startupLog, const string& configname
       cout << ::arg().configstring(true, false);
     }
     else {
-      if (!::arg().laxFile(configname.c_str())) {
+      if (!::arg().laxFile(configname)) {
         SLOG(g_log << Logger::Warning << "Unable to open configuration file '" << configname << "'" << endl,
              startupLog->error("No such file", "Unable to open configuration file", "config_file", Logging::Loggable(configname)));
         return {1, true};
@@ -3117,6 +3195,59 @@ static pair<int, bool> doConfig(Logr::log_t startupLog, const string& configname
   return {0, false};
 }
 
+static void handleRuntimeDefaults(Logr::log_t log)
+{
+#if HAVE_FIBER_SANITIZER
+  // Asan needs more stack
+  if (::arg().asNum("stack-size") == 200000) { // the default in table.py
+    ::arg().set("stack-size", "stack size per mthread") = "600000";
+  }
+#endif
+
+  const string RUNTIME = "*runtime determined*";
+  if (::arg()["version-string"] == RUNTIME) { // i.e. not set explicitly
+    ::arg().set("version-string") = fullVersionString();
+  }
+
+  if (::arg()["server-id"] == RUNTIME) { // i.e. not set explicitly
+    auto myHostname = getHostname();
+    if (!myHostname.has_value()) {
+      SLOG(g_log << Logger::Warning << "Unable to get the hostname, NSID and id.server values will be empty" << endl,
+           log->info(Logr::Warning, "Unable to get the hostname, NSID and id.server values will be empty"));
+    }
+    ::arg().set("server-id") = myHostname.has_value() ? *myHostname : "";
+  }
+
+  if (::arg()["socket-dir"].empty()) {
+    if (::arg()["chroot"].empty()) {
+      ::arg().set("socket-dir") = std::string(LOCALSTATEDIR) + "/pdns-recursor";
+    }
+    else {
+      ::arg().set("socket-dir") = "/";
+    }
+  }
+
+  if (::arg().asNum("threads") == 1) {
+    if (::arg().mustDo("pdns-distributes-queries")) {
+      SLOG(g_log << Logger::Warning << "Only one thread, no need to distribute queries ourselves" << endl,
+           log->info(Logr::Warning, "Only one thread, no need to distribute queries ourselves"));
+      ::arg().set("pdns-distributes-queries") = "no";
+    }
+  }
+
+  if (::arg().mustDo("pdns-distributes-queries") && ::arg().asNum("distributor-threads") == 0) {
+    SLOG(g_log << Logger::Warning << "Asked to run with pdns-distributes-queries set but no distributor threads, raising to 1" << endl,
+         log->info(Logr::Warning, "Asked to run with pdns-distributes-queries set but no distributor threads, raising to 1"));
+    ::arg().set("distributor-threads") = "1";
+  }
+
+  if (!::arg().mustDo("pdns-distributes-queries") && ::arg().asNum("distributor-threads") > 0) {
+    SLOG(g_log << Logger::Warning << "Not distributing queries, setting distributor threads to 0" << endl,
+         log->info(Logr::Warning, "Not distributing queries, setting distributor threads to 0"));
+    ::arg().set("distributor-threads") = "0";
+  }
+}
+
 int main(int argc, char** argv)
 {
   g_argc = argc;
@@ -3128,7 +3259,9 @@ int main(int argc, char** argv)
   int ret = EXIT_SUCCESS;
 
   try {
-    initArgs();
+    pdns::settings::rec::defineOldStyleSettings();
+    ::arg().setDefaults();
+    g_log.toConsole(Logger::Info);
     ::arg().laxParse(argc, argv); // do a lax parse
 
     if (::arg().mustDo("version")) {
@@ -3158,9 +3291,10 @@ int main(int argc, char** argv)
     g_log.setLoglevel(s_logUrgency);
     g_log.toConsole(s_logUrgency);
 
-    string configname = ::arg()["config-dir"] + "/recursor.conf";
+    g_yamlSettings = false;
+    string configname = ::arg()["config-dir"] + "/recursor";
     if (!::arg()["config-name"].empty()) {
-      configname = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"] + ".conf";
+      configname = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"];
       g_programname += "-" + ::arg()["config-name"];
     }
     cleanSlashes(configname);
@@ -3207,18 +3341,52 @@ int main(int argc, char** argv)
 
     ::arg().setSLog(startupLog);
 
-    bool mustExit = false;
-    std::tie(ret, mustExit) = doConfig(startupLog, configname, argc, argv);
-    if (ret != 0 || mustExit) {
-      return ret;
+    const string yamlconfigname = configname + ".yml";
+    string msg;
+    pdns::rust::settings::rec::Recursorsettings settings;
+    // TODO: handle include-dir on command line
+    auto yamlstatus = pdns::settings::rec::readYamlSettings(yamlconfigname, ::arg()["include-dir"], settings, msg, startupLog);
+
+    switch (yamlstatus) {
+    case pdns::settings::rec::YamlSettingsStatus::CannotOpen:
+      SLOG(g_log << Logger::Debug << "No YAML config found for configname '" << yamlconfigname << "': " << msg << endl,
+           startupLog->error(Logr::Debug, msg, "No YAML config found", "configname", Logging::Loggable(yamlconfigname)));
+      break;
+    case pdns::settings::rec::YamlSettingsStatus::PresentButFailed:
+      SLOG(g_log << Logger::Error << "YAML config found for configname '" << yamlconfigname << "' but error ocurred processing it" << endl,
+           startupLog->error(Logr::Error, msg, "YAML config found, but error occurred processsing it", "configname", Logging::Loggable(yamlconfigname)));
+      return 1;
+      break;
+    case pdns::settings::rec::YamlSettingsStatus::OK:
+      g_yamlSettings = true;
+      SLOG(g_log << Logger::Notice << "YAML config found and processed for configname '" << yamlconfigname << "'" << endl,
+           startupLog->info(Logr::Notice, "YAML config found and processed", "configname", Logging::Loggable(yamlconfigname)));
+      pdns::settings::rec::bridgeStructToOldStyleSettings(settings);
+      break;
     }
 
-    if (!::arg().file(configname.c_str())) {
-      SLOG(g_log << Logger::Warning << "Unable to open configuration file '" << configname << "'" << endl,
-           startupLog->error("No such file", "Unable to open configuration file", "config_file", Logging::Loggable(configname)));
+    if (g_yamlSettings) {
+      bool mustExit = false;
+      std::tie(ret, mustExit) = doYamlConfig(startupLog, argc, argv);
+      if (ret != 0 || mustExit) {
+        return ret;
+      }
+    }
+
+    if (yamlstatus == pdns::settings::rec::YamlSettingsStatus::CannotOpen) {
+      configname += ".conf";
+      bool mustExit = false;
+      std::tie(ret, mustExit) = doConfig(startupLog, configname, argc, argv);
+      if (ret != 0 || mustExit) {
+        return ret;
+      }
+      if (!::arg().file(configname)) {
+        SLOG(g_log << Logger::Warning << "Unable to open configuration file '" << configname << "'" << endl,
+             startupLog->error("No such file", "Unable to open configuration file", "config_file", Logging::Loggable(configname)));
+      }
     }
 
-    // Reparse, now with config file as well
+    // Reparse, now with config file as well, both for old-style as for YAML settings
     ::arg().parse(argc, argv);
 
     g_quiet = ::arg().mustDo("quiet");
@@ -3240,32 +3408,7 @@ int main(int argc, char** argv)
       return EXIT_FAILURE;
     }
 
-    if (::arg()["socket-dir"].empty()) {
-      if (::arg()["chroot"].empty()) {
-        ::arg().set("socket-dir") = std::string(LOCALSTATEDIR) + "/pdns-recursor";
-      }
-      else {
-        ::arg().set("socket-dir") = "/";
-      }
-    }
-
-    if (::arg().asNum("threads") == 1) {
-      if (::arg().mustDo("pdns-distributes-queries")) {
-        SLOG(g_log << Logger::Warning << "Asked to run with pdns-distributes-queries set but no distributor threads, raising to 1" << endl,
-             startupLog->v(1)->info("Only one thread, no need to distribute queries ourselves"));
-        ::arg().set("pdns-distributes-queries") = "no";
-      }
-    }
-
-    if (::arg().mustDo("pdns-distributes-queries") && ::arg().asNum("distributor-threads") <= 0) {
-      SLOG(g_log << Logger::Warning << "Asked to run with pdns-distributes-queries set but no distributor threads, raising to 1" << endl,
-           startupLog->v(1)->info("Asked to run with pdns-distributes-queries set but no distributor threads, raising to 1"));
-      ::arg().set("distributor-threads") = "1";
-    }
-
-    if (!::arg().mustDo("pdns-distributes-queries")) {
-      ::arg().set("distributor-threads") = "0";
-    }
+    handleRuntimeDefaults(startupLog);
 
     g_recCache = std::make_unique<MemRecursorCache>(::arg().asNum("record-cache-shards"));
     g_negCache = std::make_unique<NegCache>(::arg().asNum("record-cache-shards") / 8);
index da2cd43cfa2011d47b4888020f11d1cc8b65c7bc..b048da71a7ff4fa5e33350a7105cb3974b2d4583 100644 (file)
@@ -192,6 +192,7 @@ extern std::unique_ptr<RecursorPacketCache> g_packetCache;
 
 using RemoteLoggerStats_t = std::unordered_map<std::string, RemoteLoggerInterface::Stats>;
 
+extern bool g_yamlSettings;
 extern bool g_logCommonErrors;
 extern size_t g_proxyProtocolMaximumSize;
 extern std::atomic<bool> g_quiet;
index 34305559f546305796bb54d5797048a4810dc6d8..dd290bb1d6218ebcf28abd6e8381ba6b20b62d0b 100644 (file)
@@ -38,6 +38,8 @@
 #include "rec-tcpout.hh"
 #include "rec-main.hh"
 
+#include "settings/cxxsettings.hh"
+
 std::pair<std::string, std::string> PrefixDashNumberCompare::prefixAndTrailingNum(const std::string& a)
 {
   auto i = a.length();
@@ -2077,7 +2079,8 @@ static RecursorControlChannel::Answer help()
           "unload-lua-script                unload Lua script\n"
           "version                          return Recursor version number\n"
           "wipe-cache domain0 [domain1] ..  wipe domain data from cache\n"
-          "wipe-cache-typed type domain0 [domain1] ..  wipe domain data with qtype from cache\n"};
+          "wipe-cache-typed type domain0 [domain1] ..  wipe domain data with qtype from cache\n"
+          "show-yaml [file]                 EXPERIMENTAL command to show yaml config derived from old-style config\n"};
 }
 
 template <typename T>
@@ -2267,7 +2270,7 @@ RecursorControlChannel::Answer RecursorControlParser::getAnswer(int socket, cons
       g_log << Logger::Error << "Unable to reload zones and forwards when chroot()'ed, requested via control channel" << endl;
       return {1, "Unable to reload zones and forwards when chroot()'ed, please restart\n"};
     }
-    return {0, reloadZoneConfiguration()};
+    return {0, reloadZoneConfiguration(g_yamlSettings)};
   }
   if (cmd == "set-ecs-minimum-ttl") {
     return {0, setMinimumECSTTL(begin, end)};
index 6e2ae91dc191ca769085aa634d86dfa51bc5216f..6264ae51220a7689fbab2ac05cb60dc47ea44429 100644 (file)
@@ -31,6 +31,7 @@
 #include "credentials.hh"
 #include "namespaces.hh"
 #include "rec_channel.hh"
+#include "settings/cxxsettings.hh"
 
 ArgvMap& arg()
 {
@@ -63,27 +64,178 @@ static void initArguments(int argc, char** argv)
     exit(arg().mustDo("help") ? 0 : 99);
   }
 
-  string configname = ::arg()["config-dir"] + "/recursor.conf";
-  if (::arg()["config-name"] != "")
-    configname = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"] + ".conf";
+  string configname = ::arg()["config-dir"] + "/recursor";
+  if (!::arg()["config-name"].empty()) {
+    configname = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"];
+  }
 
   cleanSlashes(configname);
 
-  arg().laxFile(configname.c_str());
+  const string yamlconfigname = configname + ".yml";
+  string msg;
+  pdns::rust::settings::rec::Recursorsettings settings;
 
-  arg().laxParse(argc, argv); // make sure the commandline wins
+  auto yamlstatus = pdns::settings::rec::readYamlSettings(yamlconfigname, "", settings, msg, g_slog);
+
+  switch (yamlstatus) {
+  case pdns::settings::rec::YamlSettingsStatus::CannotOpen:
+    break;
+  case pdns::settings::rec::YamlSettingsStatus::PresentButFailed:
+    cerr << "YAML config found for configname '" << yamlconfigname << "' but error ocurred processing it" << endl;
+    exit(1); // NOLINT(concurrency-mt-unsafe)
+    break;
+  case pdns::settings::rec::YamlSettingsStatus::OK:
+    cerr << "YAML config found and processed for configname '" << yamlconfigname << "'" << endl;
+    pdns::settings::rec::bridgeStructToOldStyleSettings(settings);
+    break;
+  }
 
+  if (yamlstatus == pdns::settings::rec::YamlSettingsStatus::CannotOpen) {
+    configname += ".conf";
+    arg().laxFile(configname);
+  }
+  arg().laxParse(argc, argv); // make sure the commandline wins
   if (::arg()["socket-dir"].empty()) {
-    if (::arg()["chroot"].empty())
+    if (::arg()["chroot"].empty()) {
       ::arg().set("socket-dir") = std::string(LOCALSTATEDIR) + "/pdns-recursor";
-    else
+    }
+    else {
       ::arg().set("socket-dir") = ::arg()["chroot"] + "/";
+    }
   }
   else if (!::arg()["chroot"].empty()) {
     ::arg().set("socket-dir") = ::arg()["chroot"] + "/" + ::arg()["socket-dir"];
   }
 }
 
+static std::string showIncludeYAML(::rust::String& rdirname)
+{
+  std::string msg;
+  if (rdirname.empty()) {
+    return msg;
+  }
+  const auto dirname = string(rdirname);
+
+  std::vector<std::string> confFiles;
+  ::arg().gatherIncludes(dirname, ".conf", confFiles);
+  msg += "# Found " + std::to_string(confFiles.size()) + " .conf file" + addS(confFiles.size()) + " in " + dirname + "\n";
+  for (const auto& confFile : confFiles) {
+    auto converted = pdns::settings::rec::oldStyleSettingsFileToYaml(confFile, false);
+    msg += "# Converted include-dir " + confFile + " to YAML format:\n";
+    msg += converted;
+    msg += "# Validation result: ";
+    try {
+      // Parse back and validate
+      auto settings = pdns::rust::settings::rec::parse_yaml_string(converted);
+      settings.validate();
+      msg += "OK";
+    }
+    catch (const rust::Error& err) {
+      msg += err.what();
+    }
+    msg += "\n# End of converted " + confFile + "\n#\n";
+  }
+  return msg;
+}
+
+static std::string showForwardFileYAML(const ::rust::string& rfilename)
+{
+  std::string msg;
+  if (rfilename.empty() || boost::ends_with(rfilename, ".yml")) {
+    return msg;
+  }
+  const std::string filename = string(rfilename);
+
+  msg += "# Converted " + filename + " to YAML format for recursor.forward_zones_file: \n";
+  rust::Vec<pdns::rust::settings::rec::ForwardZone> forwards;
+  pdns::settings::rec::oldStyleForwardsFileToBridgeStruct(filename, forwards);
+  auto yaml = pdns::rust::settings::rec::forward_zones_to_yaml_string(forwards);
+  msg += std::string(yaml);
+  msg += "# Validation result: ";
+  try {
+    pdns::rust::settings::rec::validate_forward_zones("forward_zones", forwards);
+    msg += "OK";
+  }
+  catch (const rust::Error& err) {
+    msg += err.what();
+  }
+  msg += "\n# End of converted " + filename + "\n#\n";
+  return msg;
+}
+
+static std::string showAllowYAML(const ::rust::String& rfilename, const string& section, const string& key, const std::function<void(const ::rust::String&, const ::rust::Vec<::rust::String>&)>& func)
+{
+  std::string msg;
+  if (rfilename.empty() || boost::ends_with(rfilename, ".yml")) {
+    return msg;
+  }
+  const std::string filename = string(rfilename);
+
+  msg += "# Converted " + filename + " to YAML format for " + section + "." + key + ": \n";
+  rust::Vec<::rust::String> allows;
+  pdns::settings::rec::oldStyleAllowFileToBridgeStruct(filename, allows);
+  auto yaml = pdns::rust::settings::rec::allow_from_to_yaml_string(allows);
+  msg += std::string(yaml);
+  msg += "# Validation result: ";
+  try {
+    func(key, allows);
+    msg += "OK";
+  }
+  catch (const rust::Error& err) {
+    msg += err.what();
+  }
+  msg += "\n# End of converted " + filename + "\n#\n";
+  return msg;
+}
+
+static RecursorControlChannel::Answer showYAML(const std::string& path)
+{
+  string configName = ::arg()["config-dir"] + "/recursor.conf";
+  if (!::arg()["config-name"].empty()) {
+    configName = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"] + ".conf";
+  }
+  if (!path.empty()) {
+    configName = path;
+  }
+  cleanSlashes(configName);
+
+  try {
+    std::string msg;
+    auto converted = pdns::settings::rec::oldStyleSettingsFileToYaml(configName, true);
+    msg += "# THIS IS A PROOF OF CONCEPT! STRUCTURE, TYPES AND NAMES ARE SUBJECT TO CHANGE\n";
+    msg += "# Start of converted recursor.yml based on " + configName + "\n";
+    msg += converted;
+    msg += "# Validation result: ";
+    pdns::rust::settings::rec::Recursorsettings mainsettings;
+    try {
+      // Parse back and validate
+      mainsettings = pdns::rust::settings::rec::parse_yaml_string(converted);
+      mainsettings.validate();
+      msg += "OK";
+    }
+    catch (const rust::Error& err) {
+      msg += err.what();
+    }
+    msg += "\n# End of converted " + configName + "\n#\n";
+
+    msg += showIncludeYAML(mainsettings.recursor.include_dir);
+    msg += showForwardFileYAML(mainsettings.recursor.forward_zones_file);
+    msg += showAllowYAML(mainsettings.incoming.allow_from_file, "incoming", "allow_from_file", pdns::rust::settings::rec::validate_allow_from);
+    msg += showAllowYAML(mainsettings.incoming.allow_notify_from_file, "incoming", "allow_notify_from_file", pdns::rust::settings::rec::validate_allow_from);
+    msg += showAllowYAML(mainsettings.incoming.allow_notify_for_file, "incoming", "allow_notify_for_file", pdns::rust::settings::rec::validate_allow_for);
+    return {0, msg};
+  }
+  catch (const rust::Error& err) {
+    return {1, std::string(err.what())};
+  }
+  catch (const PDNSException& err) {
+    return {1, std::string(err.reason)};
+  }
+  catch (const std::exception& err) {
+    return {1, std::string(err.what())};
+  }
+}
+
 int main(int argc, char** argv)
 {
   g_slogStructured = false;
@@ -114,7 +266,13 @@ int main(int argc, char** argv)
 
     const vector<string>& commands = arg().getCommands();
 
-    if (commands.size() >= 1 && commands.at(0) == "hash-password") {
+    if (!commands.empty() && commands.at(0) == "show-yaml") {
+      auto [ret, str] = showYAML(commands.size() > 1 ? commands.at(1) : "");
+      cout << str << endl;
+      return ret;
+    }
+
+    if (!commands.empty() && commands.at(0) == "hash-password") {
       uint64_t workFactor = CredentialsHolder::s_defaultWorkFactor;
       if (commands.size() > 1) {
         try {
index 907a89fbe4b0b872ed1d3c0fb4112339591bfd0d..5b603c4da4dbc10232232fb3e6442183a5011ffe 100644 (file)
 #include "config.h"
 #endif
 
+#include <sys/stat.h>
+
 #include "reczones-helpers.hh"
 #include "arguments.hh"
 #include "dnsrecords.hh"
 #include "logger.hh"
 #include "syncres.hh"
 #include "zoneparser-tng.hh"
+#include "settings/cxxsettings.hh"
 
 extern int g_argc;
 extern char** g_argv;
@@ -104,49 +107,75 @@ static void* pleaseUseNewSDomainsMap(std::shared_ptr<SyncRes::domainmap_t> newma
   return 0;
 }
 
-string reloadZoneConfiguration()
+string reloadZoneConfiguration(bool yaml)
 {
   std::shared_ptr<SyncRes::domainmap_t> original = SyncRes::getDomainMap();
   auto log = g_slog->withName("config");
 
+  string configname = ::arg()["config-dir"] + "/recursor";
+  if (!::arg()["config-name"].empty()) {
+    configname = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"];
+  }
+  cleanSlashes(configname);
+
   try {
     SLOG(g_log << Logger::Warning << "Reloading zones, purging data from cache" << endl,
          log->info(Logr::Notice, "Reloading zones, purging data from cache"));
 
-    string configname = ::arg()["config-dir"] + "/recursor.conf";
-    if (::arg()["config-name"] != "") {
-      configname = ::arg()["config-dir"] + "/recursor-" + ::arg()["config-name"] + ".conf";
-    }
-    cleanSlashes(configname);
-
-    if (!::arg().preParseFile(configname.c_str(), "forward-zones"))
-      throw runtime_error("Unable to re-parse configuration file '" + configname + "'");
-    ::arg().preParseFile(configname.c_str(), "forward-zones-file");
-    ::arg().preParseFile(configname.c_str(), "forward-zones-recurse");
-    ::arg().preParseFile(configname.c_str(), "auth-zones");
-    ::arg().preParseFile(configname.c_str(), "allow-notify-for");
-    ::arg().preParseFile(configname.c_str(), "allow-notify-for-file");
-    ::arg().preParseFile(configname.c_str(), "export-etc-hosts", "off");
-    ::arg().preParseFile(configname.c_str(), "serve-rfc1918");
-    ::arg().preParseFile(configname.c_str(), "include-dir");
-    ::arg().preParse(g_argc, g_argv, "include-dir");
-
-    // then process includes
-    std::vector<std::string> extraConfigs;
-    ::arg().gatherIncludes(extraConfigs);
-
-    for (const std::string& fn : extraConfigs) {
-      if (!::arg().preParseFile(fn.c_str(), "forward-zones", ::arg()["forward-zones"]))
-        throw runtime_error("Unable to re-parse configuration file include '" + fn + "'");
-      ::arg().preParseFile(fn.c_str(), "forward-zones-file", ::arg()["forward-zones-file"]);
-      ::arg().preParseFile(fn.c_str(), "forward-zones-recurse", ::arg()["forward-zones-recurse"]);
-      ::arg().preParseFile(fn.c_str(), "auth-zones", ::arg()["auth-zones"]);
-      ::arg().preParseFile(fn.c_str(), "allow-notify-for", ::arg()["allow-notify-for"]);
-      ::arg().preParseFile(fn.c_str(), "allow-notify-for-file", ::arg()["allow-notify-for-file"]);
-      ::arg().preParseFile(fn.c_str(), "export-etc-hosts", ::arg()["export-etc-hosts"]);
-      ::arg().preParseFile(fn.c_str(), "serve-rfc1918", ::arg()["serve-rfc1918"]);
+    if (yaml) {
+      configname += ".yml";
+      string msg;
+      pdns::rust::settings::rec::Recursorsettings settings;
+      // XXX Does ::arg()["include-dir"] have the right value, i.e. potentially overriden by command line?
+      auto yamlstatus = pdns::settings::rec::readYamlSettings(configname, ::arg()["include-dir"], settings, msg, log);
+
+      switch (yamlstatus) {
+      case pdns::settings::rec::YamlSettingsStatus::CannotOpen:
+        throw runtime_error("Unable to open '" + configname + "': " + msg);
+        break;
+      case pdns::settings::rec::YamlSettingsStatus::PresentButFailed:
+        throw runtime_error("Error processing '" + configname + "': " + msg);
+        break;
+      case pdns::settings::rec::YamlSettingsStatus::OK:
+        // Does *not* set include-dir
+        pdns::settings::rec::setArgsForZoneRelatedSettings(settings);
+        break;
+      }
     }
+    else {
+      configname += ".conf";
 
+      if (!::arg().preParseFile(configname, "forward-zones")) {
+        throw runtime_error("Unable to re-parse configuration file '" + configname + "'");
+      }
+      ::arg().preParseFile(configname, "forward-zones-file");
+      ::arg().preParseFile(configname, "forward-zones-recurse");
+      ::arg().preParseFile(configname, "auth-zones");
+      ::arg().preParseFile(configname, "allow-notify-for");
+      ::arg().preParseFile(configname, "allow-notify-for-file");
+      ::arg().preParseFile(configname, "export-etc-hosts", "off");
+      ::arg().preParseFile(configname, "serve-rfc1918");
+      ::arg().preParseFile(configname, "include-dir");
+      ::arg().preParse(g_argc, g_argv, "include-dir");
+
+      // then process includes
+      std::vector<std::string> extraConfigs;
+      ::arg().gatherIncludes(::arg()["include-dir"], ".conf", extraConfigs);
+
+      for (const std::string& filename : extraConfigs) {
+        if (!::arg().preParseFile(filename, "forward-zones", ::arg()["forward-zones"])) {
+          throw runtime_error("Unable to re-parse configuration file include '" + filename + "'");
+        }
+        ::arg().preParseFile(filename, "forward-zones-file", ::arg()["forward-zones-file"]);
+        ::arg().preParseFile(filename, "forward-zones-recurse", ::arg()["forward-zones-recurse"]);
+        ::arg().preParseFile(filename, "auth-zones", ::arg()["auth-zones"]);
+        ::arg().preParseFile(filename, "allow-notify-for", ::arg()["allow-notify-for"]);
+        ::arg().preParseFile(filename, "allow-notify-for-file", ::arg()["allow-notify-for-file"]);
+        ::arg().preParseFile(filename, "export-etc-hosts", ::arg()["export-etc-hosts"]);
+        ::arg().preParseFile(filename, "serve-rfc1918", ::arg()["serve-rfc1918"]);
+      }
+    }
+    // Process command line args potentially overriding what we read from config files
     ::arg().preParse(g_argc, g_argv, "forward-zones");
     ::arg().preParse(g_argc, g_argv, "forward-zones-file");
     ::arg().preParse(g_argc, g_argv, "forward-zones-recurse");
@@ -156,31 +185,31 @@ string reloadZoneConfiguration()
     ::arg().preParse(g_argc, g_argv, "export-etc-hosts");
     ::arg().preParse(g_argc, g_argv, "serve-rfc1918");
 
-    auto [newDomainMap, newNotifySet] = parseZoneConfiguration();
+    auto [newDomainMap, newNotifySet] = parseZoneConfiguration(yaml);
 
     // purge both original and new names
     std::set<DNSName> oldAndNewDomains;
-    for (const auto& i : *newDomainMap) {
-      oldAndNewDomains.insert(i.first);
+    for (const auto& entry : *newDomainMap) {
+      oldAndNewDomains.insert(entry.first);
     }
 
     if (original) {
-      for (const auto& i : *original) {
-        oldAndNewDomains.insert(i.first);
+      for (const auto& entry : *original) {
+        oldAndNewDomains.insert(entry.first);
       }
     }
 
     // these explicitly-named captures should not be necessary, as lambda
     // capture of tuple-like structured bindings is permitted, but some
     // compilers still don't allow it
-    broadcastFunction([dm = newDomainMap] { return pleaseUseNewSDomainsMap(dm); });
-    broadcastFunction([ns = newNotifySet] { return pleaseSupplantAllowNotifyFor(ns); });
+    broadcastFunction([dmap = newDomainMap] { return pleaseUseNewSDomainsMap(dmap); });
+    broadcastFunction([nsset = newNotifySet] { return pleaseSupplantAllowNotifyFor(nsset); });
 
     // Wipe the caches *after* the new auth domain info has been set
     // up, as a query during setting up might fill the caches
     // again. Old code did the clear before, exposing a race.
-    for (const auto& i : oldAndNewDomains) {
-      wipeCaches(i, true, 0xffff);
+    for (const auto& entry : oldAndNewDomains) {
+      wipeCaches(entry, true, 0xffff);
     }
     return "ok\n";
   }
@@ -199,77 +228,149 @@ string reloadZoneConfiguration()
   return "reloading failed, see log\n";
 }
 
-std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>> parseZoneConfiguration()
+static void readAuthZoneData(SyncRes::AuthDomain& authDomain, const pair<string, string>& headers, Logr::log_t log)
 {
-  auto log = g_slog->withName("config");
-
-  TXTRecordContent::report();
-  OPTRecordContent::report();
+  SLOG(g_log << Logger::Notice << "Parsing authoritative data for zone '" << headers.first << "' from file '" << headers.second << "'" << endl,
+       log->info(Logr::Notice, "Parsing authoritative data from file", "zone", Logging::Loggable(headers.first), "file", Logging::Loggable(headers.second)));
+  ZoneParserTNG zpt(headers.second, DNSName(headers.first));
+  zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
+  zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
+  DNSResourceRecord resourceRecord;
+  DNSRecord dnsrecord;
+  while (zpt.get(resourceRecord)) {
+    try {
+      dnsrecord = DNSRecord(resourceRecord);
+      dnsrecord.d_place = DNSResourceRecord::ANSWER;
+    }
+    catch (std::exception& e) {
+      throw PDNSException("Error parsing record '" + resourceRecord.qname.toLogString() + "' of type " + resourceRecord.qtype.toString() + " in zone '" + headers.first + "' from file '" + headers.second + "': " + e.what());
+    }
+    catch (...) {
+      throw PDNSException("Error parsing record '" + resourceRecord.qname.toLogString() + "' of type " + resourceRecord.qtype.toString() + " in zone '" + headers.first + "' from file '" + headers.second + "'");
+    }
 
-  auto newMap = std::make_shared<SyncRes::domainmap_t>();
-  auto newSet = std::make_shared<notifyset_t>();
+    authDomain.d_records.insert(dnsrecord);
+  }
+}
 
-  typedef vector<string> parts_t;
-  parts_t parts;
-  const char* option_names[3] = {"auth-zones", "forward-zones", "forward-zones-recurse"};
-  for (int n = 0; n < 3; ++n) {
-    parts.clear();
-    stringtok(parts, ::arg()[option_names[n]], " ,\t\n\r");
-    for (parts_t::const_iterator iter = parts.begin(); iter != parts.end(); ++iter) {
-      SyncRes::AuthDomain ad;
-      if ((*iter).find('=') == string::npos)
-        throw PDNSException("Error parsing '" + *iter + "', missing =");
-      pair<string, string> headers = splitField(*iter, '=');
+static void processForwardZones(shared_ptr<SyncRes::domainmap_t>& newMap, Logr::log_t log)
+{
+  const std::array<string, 3> option_names = {"auth-zones", "forward-zones", "forward-zones-recurse"};
+
+  for (size_t option = 0; option < option_names.size(); ++option) {
+    vector<string> parts;
+    stringtok(parts, ::arg()[option_names.at(option)], " ,\t\n\r");
+    for (const auto& part : parts) {
+      SyncRes::AuthDomain authDomain;
+      if (part.find('=') == string::npos) {
+        throw PDNSException("Error parsing '" + part + "', missing =");
+      }
+      pair<string, string> headers = splitField(part, '=');
       boost::trim(headers.first);
       boost::trim(headers.second);
-      // headers.first=toCanonic("", headers.first);
-      if (n == 0) {
-        ad.d_rdForward = false;
-        SLOG(g_log << Logger::Notice << "Parsing authoritative data for zone '" << headers.first << "' from file '" << headers.second << "'" << endl,
-             log->info(Logr::Notice, "Parsing authoritative data from file", "zone", Logging::Loggable(headers.first), "file", Logging::Loggable(headers.second)));
-        ZoneParserTNG zpt(headers.second, DNSName(headers.first));
-        zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
-        zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
-        DNSResourceRecord rr;
-        DNSRecord dr;
-        while (zpt.get(rr)) {
-          try {
-            dr = DNSRecord(rr);
-            dr.d_place = DNSResourceRecord::ANSWER;
-          }
-          catch (std::exception& e) {
-            throw PDNSException("Error parsing record '" + rr.qname.toLogString() + "' of type " + rr.qtype.toString() + " in zone '" + headers.first + "' from file '" + headers.second + "': " + e.what());
-          }
-          catch (...) {
-            throw PDNSException("Error parsing record '" + rr.qname.toLogString() + "' of type " + rr.qtype.toString() + " in zone '" + headers.first + "' from file '" + headers.second + "'");
-          }
-
-          ad.d_records.insert(dr);
-        }
+
+      if (option == 0) {
+        authDomain.d_rdForward = false;
+        readAuthZoneData(authDomain, headers, log);
       }
       else {
-        ad.d_rdForward = (n == 2);
-        convertServersForAD(headers.first, headers.second, ad, ";", log);
+        authDomain.d_rdForward = (option == 2);
+        convertServersForAD(headers.first, headers.second, authDomain, ";", log);
       }
 
-      ad.d_name = DNSName(headers.first);
-      (*newMap)[ad.d_name] = ad;
+      authDomain.d_name = DNSName(headers.first);
+      (*newMap)[authDomain.d_name] = authDomain;
     }
   }
+}
+
+static void processApiZonesFile(shared_ptr<SyncRes::domainmap_t>& newMap, shared_ptr<notifyset_t>& newSet, Logr::log_t log)
+{
+  if (::arg()["api-config-dir"].empty()) {
+    return;
+  }
+  const auto filename = ::arg()["api-config-dir"] + "/apizones";
+  struct stat statStruct
+  {
+  };
+  // It's a TOCTU, but a harmless one
+  if (stat(filename.c_str(), &statStruct) != 0) {
+    return;
+  }
+
+  SLOG(g_log << Logger::Notice << "Processing ApiZones YAML settings from " << filename << endl,
+       log->info(Logr::Notice, "Processing ApiZones YAML settings", "path", Logging::Loggable(filename)));
+
+  const uint64_t before = newMap->size();
 
-  if (!::arg()["forward-zones-file"].empty()) {
-    SLOG(g_log << Logger::Warning << "Reading zone forwarding information from '" << ::arg()["forward-zones-file"] << "'" << endl,
-         log->info(Logr::Notice, "Reading zone forwarding information", "file", Logging::Loggable(::arg()["forward-zones-file"])));
-    auto fp = std::unique_ptr<FILE, int (*)(FILE*)>(fopen(::arg()["forward-zones-file"].c_str(), "r"), fclose);
-    if (!fp) {
-      throw PDNSException("Error opening forward-zones-file '" + ::arg()["forward-zones-file"] + "': " + stringerror());
+  std::unique_ptr<pdns::rust::settings::rec::ApiZones> zones = pdns::rust::settings::rec::api_read_zones(filename);
+  zones->validate("apizones");
+
+  for (const auto& forward : zones->forward_zones) {
+    SyncRes::AuthDomain authDomain;
+    authDomain.d_name = DNSName(string(forward.zone));
+    authDomain.d_rdForward = forward.recurse;
+    for (const auto& forwarder : forward.forwarders) {
+      ComboAddress addr = parseIPAndPort(string(forwarder), 53);
+      authDomain.d_servers.emplace_back(addr);
+    }
+    (*newMap)[authDomain.d_name] = authDomain;
+    if (forward.notify_allowed) {
+      newSet->insert(authDomain.d_name);
+    }
+  }
+  for (const auto& auth : zones->auth_zones) {
+    SyncRes::AuthDomain authDomain;
+    authDomain.d_name = DNSName(string(auth.zone));
+    readAuthZoneData(authDomain, {string(auth.zone), string(auth.file)}, log);
+    (*newMap)[authDomain.d_name] = authDomain;
+  }
+  SLOG(g_log << Logger::Warning << "Done parsing " << newMap->size() - before
+             << " ApiZones YAML settings from file '"
+             << filename << "'" << endl,
+       log->info(Logr::Notice, "Done parsing ApiZones YAML from file", "file",
+                 Logging::Loggable(filename), "count",
+                 Logging::Loggable(newMap->size() - before)));
+}
+
+static void processForwardZonesFile(shared_ptr<SyncRes::domainmap_t>& newMap, shared_ptr<notifyset_t>& newSet, Logr::log_t log)
+{
+  const auto filename = ::arg()["forward-zones-file"];
+  if (filename.empty()) {
+    return;
+  }
+  const uint64_t before = newMap->size();
+
+  if (boost::ends_with(filename, ".yml")) {
+    ::rust::Vec<pdns::rust::settings::rec::ForwardZone> vec;
+    pdns::settings::rec::readYamlForwardZonesFile(filename, vec, log);
+    for (const auto& forward : vec) {
+      SyncRes::AuthDomain authDomain;
+      authDomain.d_name = DNSName(string(forward.zone));
+      authDomain.d_rdForward = forward.recurse;
+      for (const auto& forwarder : forward.forwarders) {
+        ComboAddress addr = parseIPAndPort(string(forwarder), 53);
+        authDomain.d_servers.emplace_back(addr);
+      }
+      (*newMap)[authDomain.d_name] = authDomain;
+      if (forward.notify_allowed) {
+        newSet->insert(authDomain.d_name);
+      }
+    }
+  }
+  else {
+    SLOG(g_log << Logger::Warning << "Reading zone forwarding information from '" << filename << "'" << endl,
+         log->info(Logr::Notice, "Reading zone forwarding information", "file", Logging::Loggable(filename)));
+    auto filePtr = std::unique_ptr<FILE, int (*)(FILE*)>(fopen(filename.c_str(), "r"), fclose);
+    if (!filePtr) {
+      int err = errno;
+      throw PDNSException("Error opening forward-zones-file '" + filename + "': " + stringerror(err));
     }
 
     string line;
     int linenum = 0;
-    uint64_t before = newMap->size();
-    while (linenum++, stringfgets(fp.get(), line)) {
-      SyncRes::AuthDomain ad;
+    while (linenum++, stringfgets(filePtr.get(), line)) {
+      SyncRes::AuthDomain authDomain;
       boost::trim(line);
       if (line[0] == '#') { // Comment line, skip to the next line
         continue;
@@ -284,7 +385,7 @@ std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>>
         if (instructions.empty()) { // empty line
           continue;
         }
-        throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + ::arg()["forward-zones-file"]);
+        throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + filename);
       }
 
       bool allowNotifyFor = false;
@@ -292,7 +393,7 @@ std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>>
       for (; !domain.empty(); domain.erase(0, 1)) {
         switch (domain[0]) {
         case '+':
-          ad.d_rdForward = true;
+          authDomain.d_rdForward = true;
           continue;
         case '^':
           allowNotifyFor = true;
@@ -302,101 +403,148 @@ std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>>
       }
 
       if (domain.empty()) {
-        throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + ::arg()["forward-zones-file"]);
+        throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + filename);
       }
 
       try {
-        convertServersForAD(domain, instructions, ad, ",; ", log, false);
+        convertServersForAD(domain, instructions, authDomain, ",; ", log, false);
       }
       catch (...) {
-        throw PDNSException("Conversion error parsing line " + std::to_string(linenum) + " of " + ::arg()["forward-zones-file"]);
+        throw PDNSException("Conversion error parsing line " + std::to_string(linenum) + " of " + filename);
       }
 
-      ad.d_name = DNSName(domain);
-      (*newMap)[ad.d_name] = ad;
+      authDomain.d_name = DNSName(domain);
+      (*newMap)[authDomain.d_name] = authDomain;
       if (allowNotifyFor) {
-        newSet->insert(ad.d_name);
+        newSet->insert(authDomain.d_name);
       }
     }
-    SLOG(g_log << Logger::Warning << "Done parsing " << newMap->size() - before
-               << " forwarding instructions from file '"
-               << ::arg()["forward-zones-file"] << "'" << endl,
-         log->info(Logr::Notice, "Done parsing forwarding instructions from file", "file",
-                   Logging::Loggable(::arg()["forward-zones-file"]), "count",
-                   Logging::Loggable(newMap->size() - before)));
   }
+  SLOG(g_log << Logger::Warning << "Done parsing " << newMap->size() - before
+             << " forwarding instructions from file '"
+             << filename << "'" << endl,
+       log->info(Logr::Notice, "Done parsing forwarding instructions from file", "file",
+                 Logging::Loggable(filename), "count",
+                 Logging::Loggable(newMap->size() - before)));
+}
 
-  if (::arg().mustDo("export-etc-hosts")) {
-    string fname = ::arg()["etc-hosts-file"];
-    ifstream ifs(fname.c_str());
-    if (!ifs) {
-      SLOG(g_log << Logger::Warning << "Could not open " << fname << " for reading" << endl,
-           log->error(Logr::Warning, "Could not open file for reading", "file", Logging::Loggable(fname)));
+static void processExportEtcHosts(std::shared_ptr<SyncRes::domainmap_t>& newMap, Logr::log_t log)
+{
+  if (!::arg().mustDo("export-etc-hosts")) {
+    return;
+  }
+  string fname = ::arg()["etc-hosts-file"];
+  ifstream ifs(fname);
+  if (!ifs) {
+    SLOG(g_log << Logger::Warning << "Could not open " << fname << " for reading" << endl,
+         log->error(Logr::Warning, "Could not open file for reading", "file", Logging::Loggable(fname)));
+    return;
+  }
+  vector<string> parts;
+  std::string line{};
+  while (getline(ifs, line)) {
+    if (!parseEtcHostsLine(parts, line)) {
+      continue;
     }
-    else {
-      std::string line{};
-      while (getline(ifs, line)) {
-        if (!parseEtcHostsLine(parts, line)) {
-          continue;
-        }
 
-        try {
-          string searchSuffix = ::arg()["export-etc-hosts-search-suffix"];
-          addForwardAndReverseLookupEntries(*newMap, searchSuffix, parts, log);
-        }
-        catch (const PDNSException& ex) {
-          SLOG(g_log << Logger::Warning
-                     << "The line `" << line << "` "
-                     << "in the provided etc-hosts file `" << fname << "` "
-                     << "could not be added: " << ex.reason << ". Going to skip it."
-                     << endl,
-               log->info(Logr::Notice, "Skipping line in etc-hosts file",
-                         "line", Logging::Loggable(line),
-                         "hosts-file", Logging::Loggable(fname),
-                         "reason", Logging::Loggable(ex.reason)));
-        }
-      }
+    try {
+      string searchSuffix = ::arg()["export-etc-hosts-search-suffix"];
+      addForwardAndReverseLookupEntries(*newMap, searchSuffix, parts, log);
+    }
+    catch (const PDNSException& ex) {
+      SLOG(g_log << Logger::Warning
+                 << "The line `" << line << "` "
+                 << "in the provided etc-hosts file `" << fname << "` "
+                 << "could not be added: " << ex.reason << ". Going to skip it."
+                 << endl,
+           log->info(Logr::Notice, "Skipping line in etc-hosts file",
+                     "line", Logging::Loggable(line),
+                     "hosts-file", Logging::Loggable(fname),
+                     "reason", Logging::Loggable(ex.reason)));
     }
   }
+}
 
-  if (::arg().mustDo("serve-rfc1918")) {
-    SLOG(g_log << Logger::Warning << "Inserting rfc 1918 private space zones" << endl,
-         log->info(Logr::Notice, "Inserting rfc 1918 private space zones"));
+static void processServeRFC1918(std::shared_ptr<SyncRes::domainmap_t>& newMap, Logr::log_t log)
+{
+  if (!::arg().mustDo("serve-rfc1918")) {
+    return;
+  }
+  SLOG(g_log << Logger::Warning << "Inserting rfc 1918 private space zones" << endl,
+       log->info(Logr::Notice, "Inserting rfc 1918 private space zones"));
 
-    makePartialIPZone(*newMap, {"127"}, log);
-    makePartialIPZone(*newMap, {"10"}, log);
-    makePartialIPZone(*newMap, {"192", "168"}, log);
+  makePartialIPZone(*newMap, {"127"}, log);
+  makePartialIPZone(*newMap, {"10"}, log);
+  makePartialIPZone(*newMap, {"192", "168"}, log);
 
-    for (int n = 16; n < 32; n++) {
-      makePartialIPZone(*newMap, {"172", std::to_string(n).c_str()}, log);
-    }
+  for (int count = 16; count < 32; count++) {
+    makePartialIPZone(*newMap, {"172", std::to_string(count).c_str()}, log);
   }
+}
 
-  parts.clear();
+static void processAllowNotifyFor(shared_ptr<notifyset_t>& newSet)
+{
+  vector<string> parts;
   stringtok(parts, ::arg()["allow-notify-for"], " ,\t\n\r");
   for (auto& part : parts) {
     newSet->insert(DNSName(part));
   }
+}
 
-  if (auto anff = ::arg()["allow-notify-for-file"]; !anff.empty()) {
-    SLOG(g_log << Logger::Warning << "Reading NOTIFY-allowed zones from '" << anff << "'" << endl,
-         log->info(Logr::Notice, "Reading NOTIFY-allowed zones from file", "file", Logging::Loggable(anff)));
-    auto fp = std::unique_ptr<FILE, int (*)(FILE*)>(fopen(anff.c_str(), "r"), fclose);
-    if (!fp) {
-      throw PDNSException("Error opening allow-notify-for-file '" + anff + "': " + stringerror());
+static void processAllowNotifyForFile(shared_ptr<notifyset_t>& newSet, Logr::log_t log)
+{
+  const auto filename = ::arg()["allow-notify-for-file"];
+  if (filename.empty()) {
+    return;
+  }
+  const uint64_t before = newSet->size();
+  if (boost::ends_with(filename, ".yml")) {
+    ::rust::Vec<::rust::String> vec;
+    pdns::settings::rec::readYamlAllowNotifyForFile(filename, vec, log);
+    for (const auto& name : vec) {
+      newSet->insert(DNSName(string(name)));
+    }
+  }
+  else {
+    SLOG(g_log << Logger::Warning << "Reading NOTIFY-allowed zones from '" << filename << "'" << endl,
+         log->info(Logr::Notice, "Reading NOTIFY-allowed zones from file", "file", Logging::Loggable(filename)));
+    auto filePtr = std::unique_ptr<FILE, int (*)(FILE*)>(fopen(filename.c_str(), "r"), fclose);
+    if (!filePtr) {
+      throw PDNSException("Error opening allow-notify-for-file '" + filename + "': " + stringerror());
     }
 
     string line;
-    uint64_t before = newSet->size();
-    while (stringfgets(fp.get(), line)) {
+    while (stringfgets(filePtr.get(), line)) {
       boost::trim(line);
-      if (line[0] == '#') // Comment line, skip to the next line
+      if (line[0] == '#') // Comment line, skip to the next line
         continue;
+      }
       newSet->insert(DNSName(line));
     }
-    SLOG(g_log << Logger::Warning << "Done parsing " << newSet->size() - before << " NOTIFY-allowed zones from file '" << anff << "'" << endl,
-         log->info(Logr::Notice, "Done parsing NOTIFY-allowed zones from file", "file", Logging::Loggable(anff), "count", Logging::Loggable(newSet->size() - before)));
   }
+  SLOG(g_log << Logger::Warning << "Done parsing " << newSet->size() - before << " NOTIFY-allowed zones from file '" << filename << "'" << endl,
+       log->info(Logr::Notice, "Done parsing NOTIFY-allowed zones from file", "file", Logging::Loggable(filename), "count", Logging::Loggable(newSet->size() - before)));
+}
+
+std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>> parseZoneConfiguration(bool yaml)
+{
+  auto log = g_slog->withName("config");
+
+  TXTRecordContent::report();
+  OPTRecordContent::report();
+
+  auto newMap = std::make_shared<SyncRes::domainmap_t>();
+  auto newSet = std::make_shared<notifyset_t>();
+
+  processForwardZones(newMap, log);
+  processForwardZonesFile(newMap, newSet, log);
+  if (yaml) {
+    processApiZonesFile(newMap, newSet, log);
+  }
+  processExportEtcHosts(newMap, log);
+  processServeRFC1918(newMap, log);
+  processAllowNotifyFor(newSet);
+  processAllowNotifyForFile(newSet, log);
 
   return {newMap, newSet};
 }
diff --git a/pdns/recursordist/settings/.gitignore b/pdns/recursordist/settings/.gitignore
new file mode 100644 (file)
index 0000000..c5f95e3
--- /dev/null
@@ -0,0 +1,4 @@
+/Makefile
+/Makefile.in
+/cxxsettings-generated.cc
+/settings-old-generated.rst
diff --git a/pdns/recursordist/settings/Makefile.am b/pdns/recursordist/settings/Makefile.am
new file mode 100644 (file)
index 0000000..32b322d
--- /dev/null
@@ -0,0 +1,19 @@
+EXTRA_DIST = \
+       cxxsettings-private.hh \
+       cxxsettings.hh \
+       docs-old-preamble-in.rst \
+       docs-new-preamble-in.rst \
+       generate.py \
+       rust-bridge-in.rs \
+       rust-preamble-in.rs \
+       table.py
+
+all: cxxsettings-generated.cc
+
+# It's a bit dirty that this target also generated a file inside rust/src (lib.rs)
+cxxsettings-generated.cc: table.py generate.py rust-preamble-in.rs rust-bridge-in.rs docs-old-preamble-in.rst docs-new-preamble-in.rst
+       $(PYTHON) generate.py
+
+clean-local:
+       rm -f cxxsettings-generated.cc settings-old-generated.rst
+
diff --git a/pdns/recursordist/settings/README.md b/pdns/recursordist/settings/README.md
new file mode 100644 (file)
index 0000000..9d86c6d
--- /dev/null
@@ -0,0 +1,227 @@
+SETTINGS CODE
+=============
+This directory contains the code to generate both old-style as new style (YAML) settings code.
+
+Inside this directory, there is a `rust` subdirectory that contains the Rust code and build files.
+The Rust code uses CXX for bridging between C++ and Rust.
+At the moment of writing, we only call Rust code from C++ and not vice versa.
+
+Additionally, the Rust Serde crate (and specifically Serde-YAML) is used to generatec code to handle YAML.
+
+The entry point for code generation is `generate.py`, which uses `table.py` to produce C++, Rust and .rst files.
+See `generate.sh` for some details about the generation process.
+This directory also contains a couple of `*-in.*` files which are included into files generated by the generation process.
+
+From the C++ point of view, several namespaces are defined:
+
+* `rust`: This namespace contains the classes defined by CXX.
+Note that it is sometimes needed to explicitly name it `::rust`, as the `pdns` namespace also has a subnamespace called `rust`.
+* `pdns::rust::settings::rec`: The classes and functions generated by CXX, callable from C++.
+* `pdns::settings::rec`: The classes and functions of the settings code implemented in C++. This is mainly the code handling the conversion of old-style definitions to new-style (and vice versa).
+
+Internally, the old-style settings are still used by the recursor code.
+Rewriting the existing code to start using the new style settings directly would have made the PR introducing the new-style settings even bigger.
+Therefore, we chose to keep the code *using* settings the same.
+If a new-style settings YAML file is encountered, it will be parsed, validated and the resulting new-style settings will be used to set the old-style settings to the values found in the YAML file.
+In the future, when old-style settings do not need to be supported any longer, the recursor itself can start to use the new style settings directly.
+
+A `rec_control show-yaml [file]` command has been added to show the conversion of old-style settings to the new-style YAML.
+
+This directory
+--------------
+* `cxxsettings-generated.cc`: generated code that implements the C++ part of the settings code.
+* `cxxsettings-private.hh`: private interface used by C++ settings code internally.
+* `cxxsettings.hh`: public interface of C++ code to be used by pdns_recursor and rec_control.
+* `cxxsupport.cc`: hand written C++ code.
+* `docs-new-preamble-in.rst`: non-generated part of new settings docs.
+* `docs-old-preamble-in.rst`: non-generated part of old settings docs.
+* `generate.py`: the Python script to produce C++, Rust and .rst files based on `table.py`.
+* `rust`: the directory containing rust code.
+* `rust-bridge-in.rs`: file included in the generated Rust code, placed inside the CXX bridge module.
+* `rust-preamble-in.rs`: file included in the generated Rust code as a preamble.
+* `table.py`: the definitions of all settings.
+
+`rust` subdirectory
+-------------------
+* `Cargo.toml`: The definition of the Rust `settings` crate, including its dependencies.
+* `build.rs`: `The custom build file used by CXX, see CXX docs.
+* `cxx.h`: The generic types used by CXX generated code.
+* `lib.rs.h`:  The project specific C++ types generated by CXX.
+* `libsettings.a`: The actual static library procuced by this crate.
+* `src`: The actual rust code, `lib.rs` is generated, `bridge.rs` and `helpers.rs` are maintained manually.
+* `target`: The `cargo` maintained Rust build directory.
+
+The YAML settings are stored in a struct called (on the C++ side) `pdns::rust::settings::rec::Recursorsettings`.
+This struct has a substruct for each section defined.
+Each section has multiple typed variables.
+Below we will tour some parts of the (generated) code.
+An example settings file in YAML format:
+
+```yaml
+dnssec:
+  log_bogus: true
+incoming:
+  listen:
+  - 0.0.0.0:5301
+  - '[::]:5301'
+logging:
+  common_errors: true
+  disable_syslog: true
+  loglevel: 6
+recursor:
+  daemon: false
+  extended_resolution_errors: true
+  socket_dir: /tmp/rec
+  threads: 4
+webservice:
+  address: 127.0.0.1
+  allow_from:
+  - 0.0.0.0/0
+  api_key: secret
+  port: 8083
+  webserver: true
+```
+
+The generated code
+------------------
+C++, Rust and docmentation generating is done by the `generate.py` Python script using `table.py` as input.
+After that, the C++ to Rust bridge code is generated by CXX.
+Lets take a look at the `log_bogus` setting.
+The source of its definition is in `table.py`:
+
+```python
+    {
+        'name' : 'log_bogus',
+        'section' : 'dnssec',
+        'oldname' : 'dnssec-log-bogus',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Log DNSSEC bogus validations',
+        'doc' : '''
+Log every DNSSEC validation failure.
+**Note**: This is not logged per-query but every time records are validated as Bogus.
+ ''',
+    },
+```
+
+The old-style documention generated for this can be found in `../docs/settings.rst`:
+
+```
+.. _setting-dnssec-log-bogus:
+
+``dnssec-log-bogus``
+~~~~~~~~~~~~~~~~~~~~
+
+-  Boolean
+-  Default: no
+
+- YAML setting: :ref:`setting-yaml-dnssec.log_bogus`
+
+Log every DNSSEC validation failure.
+**Note**: This is not logged per-query but every time records are validated as Bogus.
+```
+
+The new-style documention generated for this can be found in `../docs/yamlsettings.rst`, its name includes the section and it lists a YAML default:
+
+```
+.. _setting-yaml-dnssec.log_bogus:
+
+``dnssec.log_bogus``
+^^^^^^^^^^^^^^^^^^^^
+
+-  Boolean
+-  Default: ``false``
+
+- Old style setting: :ref:`setting-dnssec-log-bogus`
+
+Log every DNSSEC validation failure.
+**Note**: This is not logged per-query but every time records are validated as Bogus.
+```
+
+The C++ code generated from this entry can be found in `cxxsettings-generated.cc`.
+The code to define the old-style settings, which is called by the recursor very early after startup and is a replacement for the hand-written code that defines all settings in the old recursor code.
+Using generated code and generated docs makes sure the docs and the actual implementation are consistent.
+Something which was not true for the old code in all cases.
+
+```cpp
+void pdns::settings::rec::defineOldStyleSettings()
+  ...
+  ::arg().setSwitch("dnssec-log-bogus", "Log DNSSEC bogus validations") = "no";
+  ...
+```
+
+There is also code generated to assign the current value of old-style `log_bogus` value to the right field in a `Recursorsettings` struct.
+This code is used to convert the currently active settings to a YAML file (`pdns_recursor --config=diff`):
+```cpp
+void pdns::settings::rec::oldStyleSettingsToBridgeStruct(Recursorsettings& settings)
+  ...
+  settings.dnssec.log_bogus = arg().mustDo("dnssec-log-bogus");
+  ...
+```
+
+Plus code to set the old-style setting, given a new-style struct:
+
+```cpp
+void pdns::settings::rec::bridgeStructToOldStyleSettings(const Recursorsettings& settings)
+  ...
+  ::arg().set("dnssec-log-bogus") = to_arg(settings.dnssec.log_bogus);
+  ...
+```
+
+Lastly, there is code to support converting a old-style settings to a new-style struct found in `pdns::settings::rec::oldKVToBridgeStruct()`.
+This code is used to convert old style settings files to YAML (used by `rec_control show-yaml`) and to generate a yaml file with all the defaults values (used by `pdns_recursor --config=default`).
+The functions implementing that are `pdns::settings::rec::oldStyleSettingsFileToYaml` and `std::string pdns::settings::rec::defaultsToYaml()`, found in `cxxsupport.cc`.
+
+The Rust code generated by `generate.py` can be found in `rust/src/lib.rs`.
+It contains these snippets that define the `log_bogus` field in the section struct `Dnssec` and the `dnssec` field in the `Recursorsettings` struct:
+
+```rust
+pub struct Dnssec {
+   ...
+   #[serde(default, skip_serializing_if = "crate::is_default")]
+   log_bogus: bool,
+   ...
+}
+pub struct Recursorsettings {
+   ...
+   #[serde(default, skip_serializing_if = "crate::is_default")]
+   dnssec: Dnssec,
+   ...
+}
+```
+The `rust/src/lib.rs` file also contains the generated code to handle Serde defaults.
+More details on this can be found in `generate.py`.
+
+The C++ version of the `Recursorsettings` struct and its substructs can be found in the CXX generated `rust/lib.rs.h` file:
+
+```cpp
+struct Recursorsettings final {
+   ...
+   ::pdns::rust::settings::rec::Dnssec dnssec;
+   ...
+   };
+   ...
+struct Dnssec final {
+  ...
+  bool log_bogus;
+  ...
+};
+```
+
+The Rust functions callable from C++ are listed in `rust-bridge-in.rs` which gets included into `rust/src/lib.rs` by `generate.py`
+An example is the function
+
+```rust
+  fn parse_yaml_string(str: &String) -> Result<Recursorsettings>;
+```
+
+Which parses YAML and produces a struct with all the settings.
+Settings that are not mentioned in the YAML string wil have their default value.
+
+`rust/lib.rs.h` contains the corresponding C++ prototype, defined in the `pdns::rust::settings::rec` namespace:
+
+```cpp
+::pdns::rust::settings::rec::Recursorsettings parse_yaml_string(::rust::String const &str);
+```
+
+The C++ function `pdns::settings::rec::readYamlSettings()` defined in `cxxsupport.cc` and called in `../rec-main.cc` calls the Rust function `pdns::rust::settings::rec::parse_yaml_string()` to do the actual YAML parsing.
\ No newline at end of file
diff --git a/pdns/recursordist/settings/cxxsettings-private.hh b/pdns/recursordist/settings/cxxsettings-private.hh
new file mode 100644 (file)
index 0000000..9c049d0
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include <cinttypes>
+#include <sstream>
+#include <variant>
+#include <vector>
+
+#include "rust/lib.rs.h"
+#include "misc.hh"
+
+using pdns::rust::settings::rec::AuthZone;
+using pdns::rust::settings::rec::ForwardZone;
+using pdns::rust::settings::rec::Recursorsettings;
+
+namespace pdns::settings::rec
+{
+::rust::Vec<::rust::String> getStrings(const std::string& name);
+::rust::Vec<ForwardZone> getForwardZones(const std::string& name);
+::rust::Vec<AuthZone> getAuthZones(const std::string& name);
+
+inline std::string to_arg(bool arg)
+{
+  return arg ? "yes" : "no";
+}
+
+inline std::string to_arg(uint64_t arg)
+{
+  return std::to_string(arg);
+}
+
+inline std::string to_arg(double arg)
+{
+  return std::to_string(arg);
+}
+
+inline std::string to_arg(const ::rust::String& str)
+{
+  return std::string(str);
+}
+
+std::string to_arg(const AuthZone& authzone);
+std::string to_arg(const ForwardZone& forwardzone);
+
+template <typename T>
+std::string to_arg(const ::rust::Vec<T>& vec)
+{
+  std::ostringstream str;
+  for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
+    if (iter != vec.begin()) {
+      str << ',';
+    }
+    str << to_arg(*iter);
+  }
+  return str.str();
+}
+
+inline void to_yaml(bool& field, const std::string& val)
+{
+  field = val != "no" && val != "off";
+}
+
+inline void to_yaml(::rust::String& field, const std::string& val)
+{
+  field = val;
+}
+
+inline void to_yaml(::rust::Vec<::rust::String>& field, const std::string& val)
+{
+  stringtok(field, val, ", ;");
+}
+
+void to_yaml(uint64_t& field, const std::string& val);
+void to_yaml(double& field, const std::string& val);
+void to_yaml(::rust::Vec<AuthZone>& field, const std::string& val);
+void to_yaml(::rust::Vec<ForwardZone>& field, const std::string& val, bool recurse = false);
+}
diff --git a/pdns/recursordist/settings/cxxsettings.hh b/pdns/recursordist/settings/cxxsettings.hh
new file mode 100644 (file)
index 0000000..7577e86
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include <string>
+#include "rust/cxx.h"
+#include "rust/lib.rs.h"
+#include "logging.hh"
+
+namespace pdns::settings::rec
+{
+enum YamlSettingsStatus : uint8_t
+{
+  OK,
+  CannotOpen,
+  PresentButFailed,
+};
+
+void defineOldStyleSettings();
+void oldStyleSettingsToBridgeStruct(pdns::rust::settings::rec::Recursorsettings& settings);
+void oldStyleForwardsFileToBridgeStruct(const std::string& filename, ::rust::Vec<pdns::rust::settings::rec::ForwardZone>& vec);
+void oldStyleAllowFileToBridgeStruct(const std::string& filename, ::rust::Vec<::rust::String>& vec);
+bool oldKVToBridgeStruct(string& key, const string& value, ::rust::String& section, ::rust::String& fieldname, ::rust::String& type_name, pdns::rust::settings::rec::Value& rustvalue);
+std::string oldStyleSettingsFileToYaml(const string& fname, bool mainFile);
+std::string defaultsToYaml();
+YamlSettingsStatus readYamlSettings(const std::string& configname, const std::string& includeDirOnCommandLine, rust::settings::rec::Recursorsettings& settings, std::string& msg, Logr::log_t log);
+void bridgeStructToOldStyleSettings(const pdns::rust::settings::rec::Recursorsettings& settings);
+void readYamlForwardZonesFile(const std::string& filename, ::rust::Vec<pdns::rust::settings::rec::ForwardZone>& vec, Logr::log_t log);
+void readYamlAllowFromFile(const std::string& filename, ::rust::Vec<::rust::String>& vec, Logr::log_t log);
+void readYamlAllowNotifyForFile(const std::string& filename, ::rust::Vec<::rust::String>& vec, Logr::log_t log);
+void setArgsForZoneRelatedSettings(pdns::rust::settings::rec::Recursorsettings& settings);
+void setArgsForACLRelatedSettings(pdns::rust::settings::rec::Recursorsettings& settings);
+}
diff --git a/pdns/recursordist/settings/cxxsupport.cc b/pdns/recursordist/settings/cxxsupport.cc
new file mode 100644 (file)
index 0000000..84c3f1c
--- /dev/null
@@ -0,0 +1,512 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <fstream>
+#include <regex>
+
+#include "namespaces.hh"
+#include "arguments.hh"
+#include "misc.hh"
+#include "cxxsettings.hh"
+#include "cxxsettings-private.hh"
+#include "logger.hh"
+#include "logging.hh"
+
+::rust::Vec<::rust::String> pdns::settings::rec::getStrings(const std::string& name)
+{
+  ::rust::Vec<::rust::String> vec;
+  to_yaml(vec, arg()[name]);
+  return vec;
+}
+
+::rust::Vec<ForwardZone> pdns::settings::rec::getForwardZones(const string& name)
+{
+  ::rust::Vec<ForwardZone> vec;
+  const auto recurse = name == "forward-zones-recurse";
+  to_yaml(vec, arg()[name], recurse);
+  return vec;
+}
+
+::rust::Vec<AuthZone> pdns::settings::rec::getAuthZones(const string& name)
+{
+  ::rust::Vec<AuthZone> vec;
+  to_yaml(vec, arg()[name]);
+  return vec;
+}
+
+void pdns::settings::rec::oldStyleForwardsFileToBridgeStruct(const std::string& file, ::rust::Vec<ForwardZone>& vec)
+{
+  auto filePtr = std::unique_ptr<FILE, decltype(&fclose)>(fopen(file.c_str(), "r"), fclose);
+  if (!filePtr) {
+    throw PDNSException("Error opening forward-zones-file '" + file + "': " + stringerror());
+  }
+
+  string line;
+  int linenum = 0;
+  while (linenum++, stringfgets(filePtr.get(), line)) {
+    boost::trim(line);
+    if (line.length() == 0 || line.at(0) == '#') { // Comment line, skip to the next line
+      continue;
+    }
+    auto [domain, instructions] = splitField(line, '=');
+    instructions = splitField(instructions, '#').first; // Remove EOL comments
+    boost::trim(domain);
+    boost::trim(instructions);
+    if (domain.empty() || instructions.empty()) {
+      throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + file);
+    }
+    bool allowNotify = false;
+    bool recurse = false;
+    for (; !domain.empty(); domain.erase(0, 1)) {
+      switch (domain[0]) {
+      case '+':
+        recurse = true;
+        continue;
+      case '^':
+        allowNotify = true;
+        continue;
+      }
+      break;
+    }
+    if (domain.empty()) {
+      throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + file);
+    }
+    ::rust::Vec<::rust::String> addresses;
+    stringtok(addresses, instructions, ",; ");
+    ForwardZone forwardzone{domain, addresses, recurse, allowNotify};
+    vec.push_back(std::move(forwardzone));
+  }
+}
+
+void pdns::settings::rec::oldStyleAllowFileToBridgeStruct(const std::string& file, ::rust::Vec<::rust::String>& vec)
+{
+  string line;
+  ifstream ifs(file);
+  if (!ifs) {
+    int err = errno;
+    throw runtime_error("Could not open '" + file + "': " + stringerror(err));
+  }
+
+  while (getline(ifs, line)) {
+    auto pos = line.find('#');
+    if (pos != string::npos) {
+      line.resize(pos);
+    }
+    boost::trim(line);
+    if (line.empty()) {
+      continue;
+    }
+    vec.emplace_back(line);
+  }
+}
+
+static void mergeYamlSubFile(const std::string& configname, Recursorsettings& settings, Logr::log_t log)
+{
+  SLOG(g_log << Logger::Notice << "Processing YAML settings from " << configname << endl,
+       log->info(Logr::Notice, "Processing YAML settings", "path", Logging::Loggable(configname)));
+  auto file = ifstream(configname);
+  if (!file.is_open()) {
+    throw runtime_error("Cannot open " + configname);
+  }
+  auto data = string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+  pdns::rust::settings::rec::merge(settings, data);
+}
+
+pdns::settings::rec::YamlSettingsStatus pdns::settings::rec::readYamlSettings(const std::string& configname, const std::string& includeDirOnCommandLine, Recursorsettings& settings, std::string& msg, Logr::log_t log)
+{
+  auto file = ifstream(configname);
+  if (!file.is_open()) {
+    msg = stringerror(errno);
+    return YamlSettingsStatus::CannotOpen;
+  }
+  SLOG(g_log << Logger::Notice << "Processing main YAML settings from " << configname << endl,
+       log->info(Logr::Notice, "Processing main YAML settings", "path", Logging::Loggable(configname)));
+  try {
+    auto data = string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+    auto yamlstruct = pdns::rust::settings::rec::parse_yaml_string(data);
+    std::vector<std::string> yamlFiles;
+    ::arg().gatherIncludes(!includeDirOnCommandLine.empty() ? includeDirOnCommandLine : string(yamlstruct.recursor.include_dir),
+                           ".yml", yamlFiles);
+    for (const auto& yamlfile : yamlFiles) {
+      mergeYamlSubFile(yamlfile, yamlstruct, log);
+    }
+    yamlstruct.validate();
+    settings = yamlstruct;
+    return YamlSettingsStatus::OK;
+  }
+  catch (const ::rust::Error& ex) {
+    msg = ex.what();
+    return YamlSettingsStatus::PresentButFailed;
+  }
+  catch (const std::exception& ex) {
+    msg = ex.what();
+    return YamlSettingsStatus::PresentButFailed;
+  }
+  catch (...) {
+    msg = "Unexpected exception processing YAML";
+    return YamlSettingsStatus::PresentButFailed;
+  }
+}
+
+void pdns::settings::rec::readYamlAllowFromFile(const std::string& filename, ::rust::Vec<::rust::String>& vec, Logr::log_t log)
+{
+  SLOG(g_log << Logger::Notice << "Processing allow YAML settings from " << filename << endl,
+       log->info(Logr::Notice, "Processing allow YAML settings", "path", Logging::Loggable(filename)));
+  auto file = ifstream(filename);
+  if (!file.is_open()) {
+    throw runtime_error(stringerror(errno));
+  }
+  auto data = string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+  auto yamlvec = pdns::rust::settings::rec::parse_yaml_string_to_allow_from(data);
+  pdns::rust::settings::rec::validate_allow_from(filename, yamlvec);
+  vec = yamlvec;
+}
+
+void pdns::settings::rec::readYamlForwardZonesFile(const std::string& filename, ::rust::Vec<pdns::rust::settings::rec::ForwardZone>& vec, Logr::log_t log)
+{
+  SLOG(g_log << Logger::Notice << "Processing forwarding YAML settings from " << filename << endl,
+       log->info(Logr::Notice, "Processing forwarding YAML settings", "path", Logging::Loggable(filename)));
+  auto file = ifstream(filename);
+  if (!file.is_open()) {
+    throw runtime_error(stringerror(errno));
+  }
+  auto data = string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+  auto yamlvec = pdns::rust::settings::rec::parse_yaml_string_to_forward_zones(data);
+  pdns::rust::settings::rec::validate_forward_zones("forward_zones", yamlvec);
+  vec = yamlvec;
+}
+
+void pdns::settings::rec::readYamlAllowNotifyForFile(const std::string& filename, ::rust::Vec<::rust::String>& vec, Logr::log_t log)
+{
+  SLOG(g_log << Logger::Notice << "Processing allow-notify-for YAML settings from " << filename << endl,
+       log->info(Logr::Notice, "Processing allow-notify-for YAML settings", "path", Logging::Loggable(filename)));
+  auto file = ifstream(filename);
+  if (!file.is_open()) {
+    throw runtime_error(stringerror(errno));
+  }
+  auto data = string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+  auto yamlvec = pdns::rust::settings::rec::parse_yaml_string_to_allow_notify_for(data);
+  pdns::rust::settings::rec::validate_allow_notify_for("allow-notify-for", yamlvec);
+  vec = yamlvec;
+}
+
+std::string pdns::settings::rec::to_arg(const AuthZone& authzone)
+{
+  std::ostringstream str;
+  str << to_arg(authzone.zone) << '=' << to_arg(authzone.file);
+  return str.str();
+}
+
+std::string pdns::settings::rec::to_arg(const ForwardZone& forwardzone)
+{
+  std::ostringstream str;
+  str << to_arg(forwardzone.zone) << '=';
+  const auto& vec = forwardzone.forwarders;
+  for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
+    if (iter != vec.begin()) {
+      str << ';';
+    }
+    str << to_arg(*iter);
+  }
+  return str.str();
+}
+
+void pdns::settings::rec::setArgsForZoneRelatedSettings(Recursorsettings& settings)
+{
+  ::arg().set("forward-zones") = to_arg(settings.recursor.forward_zones);
+  ::arg().set("forward-zones-file") = to_arg(settings.recursor.forward_zones_file);
+  ::arg().set("forward-zones-recurse") = to_arg(settings.recursor.forward_zones_recurse);
+  ::arg().set("auth-zones") = to_arg(settings.recursor.auth_zones);
+  ::arg().set("allow-notify-for") = to_arg(settings.incoming.allow_notify_for);
+  ::arg().set("allow-notify-for-file") = to_arg(settings.incoming.allow_notify_for_file);
+  ::arg().set("export-etc-hosts") = to_arg(settings.recursor.export_etc_hosts);
+  ::arg().set("serve-rfc1918") = to_arg(settings.recursor.serve_rfc1918);
+}
+
+void pdns::settings::rec::setArgsForACLRelatedSettings(Recursorsettings& settings)
+{
+  ::arg().set("allow-from") = to_arg(settings.incoming.allow_from);
+  ::arg().set("allow-from-file") = to_arg(settings.incoming.allow_from_file);
+  ::arg().set("allow-notify-from") = to_arg(settings.incoming.allow_notify_from);
+  ::arg().set("allow-notify-from-file") = to_arg(settings.incoming.allow_notify_from_file);
+}
+
+void pdns::settings::rec::to_yaml(uint64_t& field, const std::string& val)
+{
+  if (val.empty()) {
+    field = 0;
+    return;
+  }
+
+  checked_stoi_into(field, val, nullptr, 0);
+}
+
+void pdns::settings::rec::to_yaml(double& field, const std::string& val)
+{
+  if (val.empty()) {
+    field = 0.0;
+    return;
+  }
+
+  const auto* cptr_orig = val.c_str();
+  char* cptr_ret = nullptr;
+  auto retval = strtod(cptr_orig, &cptr_ret);
+
+  field = retval;
+}
+
+void pdns::settings::rec::to_yaml(::rust::Vec<AuthZone>& field, const std::string& val)
+{
+  vector<string> zones;
+  stringtok(zones, val, " ,\t\n\r");
+  for (const auto& zone : zones) {
+    auto headers = splitField(zone, '=');
+    boost::trim(headers.first);
+    boost::trim(headers.second);
+    AuthZone authzone{headers.first, headers.second};
+    field.push_back(std::move(authzone));
+  }
+}
+
+void pdns::settings::rec::to_yaml(::rust::Vec<ForwardZone>& field, const std::string& val, bool recurse)
+{
+  vector<string> zones;
+  stringtok(zones, val, " ,\t\n\r");
+  for (const auto& zone : zones) {
+    auto headers = splitField(zone, '=');
+    boost::trim(headers.first);
+    boost::trim(headers.second);
+    ::rust::Vec<::rust::String> addresses;
+    stringtok(addresses, headers.second, " ;");
+    ForwardZone forwardzone{headers.first, addresses, recurse, false};
+    field.push_back(std::move(forwardzone));
+  }
+}
+
+using FieldMap = std::map<pair<::rust::String, ::rust::String>, pdns::rust::settings::rec::OldStyle>;
+
+static bool simpleRustType(const ::rust::String& rname)
+{
+  return rname == "bool" || rname == "u64" || rname == "f64" || rname == "String";
+}
+
+static void processLine(const std::string& arg, FieldMap& map, bool mainFile)
+{
+  string var;
+  string val;
+  string::size_type pos = 0;
+  bool incremental = false;
+
+  if (arg.find("--") == 0 && (pos = arg.find("+=")) != string::npos) // this is a --port+=25 case
+  {
+    var = arg.substr(2, pos - 2);
+    val = arg.substr(pos + 2);
+    incremental = true;
+  }
+  else if (arg.find("--") == 0 && (pos = arg.find('=')) != string::npos) // this is a --port=25 case
+  {
+    var = arg.substr(2, pos - 2);
+    val = arg.substr(pos + 1);
+  }
+  else if (arg.find("--") == 0 && (arg.find('=') == string::npos)) // this is a --daemon case
+  {
+    var = arg.substr(2);
+    val = "";
+  }
+  else if (arg[0] == '-' && arg.length() > 1) {
+    var = arg.substr(1);
+    val = "";
+  }
+  boost::trim(var);
+  if (var.empty()) {
+    return;
+  }
+  pos = val.find_first_not_of(" \t"); // strip leading whitespace
+  if (pos != 0 && pos != string::npos) {
+    val = val.substr(pos);
+  }
+
+  ::rust::String section;
+  ::rust::String fieldname;
+  ::rust::String type_name;
+  pdns::rust::settings::rec::Value rustvalue;
+  if (pdns::settings::rec::oldKVToBridgeStruct(var, val, section, fieldname, type_name, rustvalue)) {
+    auto overriding = !mainFile && !incremental && !simpleRustType(type_name);
+    auto [existing, inserted] = map.emplace(std::pair{std::pair{section, fieldname}, pdns::rust::settings::rec::OldStyle{section, fieldname, var, type_name, rustvalue, overriding}});
+    if (!inserted) {
+      // Simple values overwrite always
+      existing->second.value.bool_val = rustvalue.bool_val;
+      existing->second.value.u64_val = rustvalue.u64_val;
+      existing->second.value.f64_val = rustvalue.f64_val;
+      existing->second.value.string_val = rustvalue.string_val;
+      // List values only if = was used
+      if (!incremental) {
+        existing->second.value.vec_string_val.clear();
+        existing->second.value.vec_forwardzone_val.clear();
+        existing->second.value.vec_authzone_val.clear();
+      }
+      for (const auto& add : rustvalue.vec_string_val) {
+        existing->second.value.vec_string_val.emplace_back(add);
+      }
+      for (const auto& add : rustvalue.vec_forwardzone_val) {
+        existing->second.value.vec_forwardzone_val.emplace_back(add);
+      }
+      for (const auto& add : rustvalue.vec_authzone_val) {
+        existing->second.value.vec_authzone_val.emplace_back(add);
+      }
+    }
+  }
+}
+
+std::string pdns::settings::rec::oldStyleSettingsFileToYaml(const string& fname, bool mainFile)
+{
+  string line;
+  string pline;
+
+  std::ifstream configFileStream(fname);
+  if (!configFileStream) {
+    int err = errno;
+    throw runtime_error("Cannot read " + fname + ": " + stringerror(err));
+  }
+
+  // Read the old-style config file and produce a map with the data, taking into account the
+  // difference between = and +=
+  FieldMap map;
+  while (getline(configFileStream, pline)) {
+    boost::trim_right(pline);
+
+    if (!pline.empty() && pline[pline.size() - 1] == '\\') {
+      line += pline.substr(0, pline.length() - 1);
+      continue;
+    }
+
+    line += pline;
+
+    // strip everything after a #
+    string::size_type pos = line.find('#');
+    if (pos != string::npos) {
+      // make sure it's either first char or has whitespace before
+      // fixes issue #354
+      if (pos == 0 || (std::isspace(line[pos - 1]) != 0)) {
+        line = line.substr(0, pos);
+      }
+    }
+
+    // strip trailing spaces
+    boost::trim_right(line);
+
+    // strip leading spaces
+    pos = line.find_first_not_of(" \t\r\n");
+    if (pos != string::npos) {
+      line = line.substr(pos);
+    }
+
+    processLine("--" + line, map, mainFile);
+    line = "";
+  }
+
+  // Convert the map to a vector, as CXX does not have any dictionary like support.
+  ::rust::Vec<pdns::rust::settings::rec::OldStyle> vec;
+  vec.reserve(map.size());
+  for (const auto& entry : map) {
+    vec.emplace_back(entry.second);
+  }
+  return std::string(pdns::rust::settings::rec::map_to_yaml_string(vec));
+}
+
+std::string pdns::settings::rec::defaultsToYaml()
+{
+  // In this function we make use of the fact that we know a little about the formatting of YAML by
+  // serde_yaml.  ATM there's no way around that, as serde_yaml itself does not have any support for
+  // comments (other than stripping them while parsing). So produced YAML cannot have any comments.
+
+  // First we construct a map of (section, name) to info about the field (oldname, type, value etc)
+  FieldMap map;
+  for (const auto& var : arg().list()) {
+    if (const auto newname = ArgvMap::isDeprecated(var); !newname.empty()) {
+      continue;
+    }
+    ::rust::String section;
+    ::rust::String fieldname;
+    ::rust::String type_name;
+    pdns::rust::settings::rec::Value rustvalue;
+
+    string name = var;
+    string val = arg().getDefault(var);
+    if (pdns::settings::rec::oldKVToBridgeStruct(name, val, section, fieldname, type_name, rustvalue)) {
+      map.emplace(std::pair{std::pair{section, fieldname}, pdns::rust::settings::rec::OldStyle{section, fieldname, name, type_name, rustvalue, false}});
+    }
+  }
+  // Convert the map to a vector, as CXX does not have any dictionary like support.
+  ::rust::Vec<pdns::rust::settings::rec::OldStyle> vec;
+  vec.reserve(map.size());
+  for (const auto& entry : map) {
+    vec.emplace_back(entry.second);
+  }
+  const auto defs = std::string(pdns::rust::settings::rec::map_to_yaml_string(vec));
+
+  // We now have a YAML string, with all sections and all default values. Do a litle bit of parsing
+  // to insert the help text lines.
+  std::vector<std::string> lines;
+  stringtok(lines, defs, "\n");
+  std::string res;
+
+  // These two RE's know about the formatting generated by serde_yaml
+  std::regex sectionRE("^(\\w+):");
+  std::regex fieldRE("^  (\\w+):");
+  std::string section;
+  std::string field;
+
+  for (const auto& line : lines) {
+    bool withHelp = false;
+    bool sectionChange = false;
+    std::smatch matches;
+    std::regex_search(line, matches, sectionRE);
+    if (!matches.empty()) {
+      section = matches[1];
+      sectionChange = true;
+    }
+    std::regex_search(line, matches, fieldRE);
+    if (!matches.empty()) {
+      field = matches[1];
+      withHelp = true;
+    }
+    if (withHelp) {
+      auto oldname = std::string(map.find(make_pair(section, field))->second.old_name);
+      res += "##### ";
+      res += arg().getHelp(oldname);
+      res += '\n';
+    }
+    if (sectionChange) {
+      res += "\n######### SECTION ";
+      res += section;
+      res += " #########\n";
+      res += line;
+    }
+    else {
+      res += "# ";
+      res += line;
+    }
+    res += '\n';
+  }
+  return res;
+}
diff --git a/pdns/recursordist/settings/docs-new-preamble-in.rst b/pdns/recursordist/settings/docs-new-preamble-in.rst
new file mode 100644 (file)
index 0000000..e59cb5d
--- /dev/null
@@ -0,0 +1,185 @@
+PowerDNS Recursor New Style (YAML) Settings
+===========================================
+
+Each setting can appear on the command line, prefixed by ``--`` and using the old style name, or in configuration files.
+Settings on the command line are processed after the file-based settings are processed.
+
+.. note::
+   Starting with version 5.0.0., :program:`Recursor` supports a new YAML syntax for configuration files
+   as described here.
+   A configuration using the old style syntax can be converted to a YAML configuration using the instructions in :doc:`appendices/yamlconversion`.
+   In a future release support for the "old-style" settings will be dropped.
+
+
+YAML settings file
+------------------
+Please refer to e.g. `<https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html>`_
+for a description of YAML syntax.
+
+A :program:`Recursor` configuration file has several sections. For example, ``incoming`` for
+settings related to receiving queries and ``dnssec`` for settings related to DNSSEC processing.
+
+An example :program:`Recursor` YAML configuration file looks like:
+
+.. code-block:: yaml
+
+  dnssec:
+    log_bogus: true
+  incoming:
+    listen:
+      - 0.0.0.0:5301
+      - '[::]:5301'
+  recursor:
+    extended_resolution_errors: true
+    forward_zones:
+      - zone: example.com
+        forwarders:
+          - 127.0.0.1:5301
+  outgoing:
+    query_local_address:
+      - 0.0.0.0
+      - '::'
+  logging:
+    loglevel: 6
+
+Take care when listing IPv6 addresses, as characters used for these are special to YAML.
+If in doubt, quote any string containing ``:``, ``[`` or ``]`` and use (online) tools to check your YAML syntax.
+Specify an empty sequence using ``[]``.
+
+The main setting file is called ``recursor.yml`` and will be processed first.
+This settings file might refer to other files via the `recursor.include_dir`_ setting.
+The next section will describe how settings specified in multiple files are merged.
+
+Merging multiple setting files
+------------------------------
+If `recursor.include_dir`_ is set, all ``.yml`` files in it will be processed in alphabetical order, modifying the  settings processed so far.
+
+For simple values like an boolean or number setting, a value in the processed file will overwrite an existing setting.
+
+For values of type sequence, the new value will *replace* the existing value if the existing value is equal to the ``default`` or if the new value is marked with the ``!override`` tag.
+Otherwise, the existing value will be *extended* with the new value by appending the new sequence to the existing.
+
+For example, with the above example ``recursor.yml`` and an include directory containing a file ``extra.yml``:
+
+.. code-block:: yaml
+
+  dnssec:
+    log_bogus: false
+  recursor:
+    forward_zones:
+      - zone: example.net
+        forwarders:
+          - '::1'
+  outgoing:
+     query_local_address: !override
+       - 0.0.0.0
+     dont_query: []
+
+After merging, ``dnssec.log_bogus`` will be ``false``, the sequence of ``recursor.forward_zones`` will contain 2 zones and the ``outgoing`` addresses used will contain one entry, as the ``extra.yml`` entry has overwritten the existing one.
+
+``outgoing.dont-query`` has a non-empty sequence as default value. The main ``recursor.yml`` did not set it, so before processing ``extra.yml`` had the default value.
+After processing ``extra.yml`` the value will be set to the empty sequence, as existing default values are overwritten by new values.
+
+.. warning::
+   The merging process does not process values deeper than the second level.
+   For example if the main ``recursor.yml`` specified a forward zone
+
+   .. code-block:: yaml
+
+     forward_zones:
+       - zone: example.net
+         forwarders:
+           - '::1'
+
+   and another settings file contains
+
+   .. code-block:: yaml
+
+     forward_zones:
+       - zone: example.net
+         forwarders:
+           - '::2'
+
+   The result will *not* be a a single forward with two IP addresses, but two entries for ``example.net``.
+   It depends on the specific setting how the sequence is processed further.
+   In the future we might add a check for this case.
+
+Socket Address
+^^^^^^^^^^^^^^
+A socket address is either an IP or and IP:port combination
+For example:
+
+.. code-block:: yaml
+
+   some_key: 127.0.0.1
+   another_key: '[::1]:8080'
+
+Subnet
+^^^^^^
+A subnet is a single IP address or an IP address followed by a slash and a prefix length.
+If no prefix length is specified, ``/32`` or ``/128`` is assumed, indicating a single IP address.
+Subnets can also be prefixed with a ``!``, specifying negation.
+This can be used to deny addresses from a previously allowed range.
+
+For example, ``alow-from`` takes a sequence of subnets:
+
+.. code-block:: yaml
+
+   allow_from:
+     - '2001:DB8::/32'
+     - 128.66.0.0/16
+     - !128.66.1.2
+
+In this case the address ``128.66.1.2`` is excluded from the addresses allowed access.
+
+Forward Zone
+^^^^^^^^^^^^
+A forward zone is defined as:
+
+.. code-block:: yaml
+
+  zone: zonename
+  forwarders:
+    - Socket Address
+    - ...
+  recurse: Boolean, default false
+  allow_notify:  Boolean, default false
+
+An example of a ``forward_zones`` entry, which consists of a sequence of forward zone entries:
+
+.. code-block:: yaml
+
+  - zone: example1.com
+    forwarders:
+      - 127.0.0.1
+      - 127.0.0.1:5353
+      - '[::1]53'
+  - zone: example2.com
+    forwarders:
+      - '::1'
+    recurse: true
+    notify_allowed: true
+
+
+Auth Zone
+^^^^^^^^^
+A auth zone is defined as:
+
+.. code-block:: yaml
+
+  zone: name
+  file: filename
+
+An example of a ``auth_zones`` entry, consisting of a sequence of auth zones:
+
+.. code-block:: yaml
+
+   auth_zones:
+     - zone: example.com
+       file: zones/example.com.zone
+     - zone: example.net
+       file: zones/example.net.zone
+
+The YAML settings
+-----------------
+
diff --git a/pdns/recursordist/settings/docs-old-preamble-in.rst b/pdns/recursordist/settings/docs-old-preamble-in.rst
new file mode 100644 (file)
index 0000000..19ca61d
--- /dev/null
@@ -0,0 +1,39 @@
+PowerDNS Recursor Settings
+==========================
+Each setting can appear on the command line, prefixed by ``--``, or in the configuration file.
+The command line overrides the configuration file.
+
+.. note::
+   Starting with version 5.0.0, :program:`Recursor` supports a new YAML syntax for configuration files.
+   A configuration using the old style syntax can be converted to a YAML configuration using the instructions in :doc:`appendices/yamlconversion`.
+   In a future release support for the "old-style" settings will be dropped.
+
+.. note::
+   Settings marked as ``Boolean`` can either be set to an empty value, which means **on**, or to ``no`` or ``off`` which means **off**.
+   Anything else means **on**.
+
+   For example:
+
+   - ``serve-rfc1918`` on its own means: do serve those zones.
+   - ``serve-rfc1918 = off`` or ``serve-rfc1918 = no`` means: do not serve those zones.
+   - Anything else means: do serve those zones.
+
+You can use ``+=`` syntax to set some variables incrementally, but this
+requires you to have at least one non-incremental setting for the
+variable to act as base setting. This is mostly useful for
+:ref:`setting-include-dir` directive. An example::
+
+  forward-zones = foo.example.com=192.168.100.1;
+  forward-zones += bar.example.com=[1234::abcde]:5353;
+
+When a list of **Netmasks** is mentioned, a list of subnets can be specified.
+A subnet that is not followed by ``/`` will be interpreted as a ``/32`` or ``/128`` subnet (a single address), depending on address family.
+For most settings, it is possible to exclude ranges by prefixing an item with the negation character ``!``.
+For example::
+
+  allow-from = 2001:DB8::/32, 128.66.0.0/16, !128.66.1.2
+
+In this case the address ``128.66.1.2`` is excluded from the addresses allowed access.
+
+The Settings
+------------
diff --git a/pdns/recursordist/settings/generate.py b/pdns/recursordist/settings/generate.py
new file mode 100644 (file)
index 0000000..b3e3d7d
--- /dev/null
@@ -0,0 +1,743 @@
+"""The Python script that takes table.py and generates C++, Rust ahd .rst code."""
+#
+# For C++ it generates cxxsettings-generated.cc containing support for old style
+# settings plus conversion from old style to new style.
+#
+# For Rust it generates rust/src/lib.rs, containing the structs with
+# CXX and Serde annotations and associated code. rust-preamble-in.rs is
+# included before the generated code and rus-bridge-in.rs inside the
+# bridge module in the generated lib.rs. Header files generated by CXX
+# (lib.rs.h and cxx.h) are copied from deep inside the generated code
+# hierarchy into the rust subdir as well.
+#
+# Two documentation files are generated: ../docs/settings.rst and
+# ../docs/yamlsettings.rst.  Each of these files have a preamble as
+# well, describing the syntax and giving some examples.
+#
+# An example table.py entry:
+#
+# {
+# 'name' : "allow_from",
+# 'section' : "incoming",
+# 'oldname' : "allow-from",
+# 'type' : LType.ListSubnets,
+# 'default' : "127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16,
+#              172.16.0.0/12, ::1/128, fc00::/7, fe80::/10",
+# 'help' : "If set, only allow these comma separated netmasks to recurse",
+# 'doc' : """
+#  """
+# }
+#
+# The fields are:
+#
+#   name: short name within section, also yaml key, must be unique within section and not a C++ or
+#            Rust keyword
+#   section: yaml section
+#   oldname: name in old style settings, must be unique globally
+#   type : logical type (leave the space before the : as pylint is buggy parsing lines with type
+#            immediately followed by colon)x
+#   default: default value, use 'true' and 'false' for Booleans
+#   help: short help text
+#   doc: the docs text will be put here
+#   doc-rst: optional .rst annotations, typically used for ..version_added/changed::
+#   doc-new: optional docs text specific to YAML format, e.g. not talking about comma separated
+#            lists and giving YAML examples. (optional)
+#   skip-yamL: optional if this key is present, the field wil be skipped in the new style settings.
+#            Use for deprecated settings, the generated code will use deprecated map from arg()
+#            to handle old names when converting .conf to .yml
+#   versionadded: string or list of strings
+#
+# The above struct will generate in cxxsettings-generated.cc:
+#
+#   ::arg().set("allow-from", "If set, only allow these comma separated netmasks to recurse") = \
+#     "127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, ...";
+#
+# For Rust, it will generate a field `allow_from' within struct Incoming.
+# As CXX places restrictions on the Serde code (which means we cannot
+# use Serde extensions) we have to handle the default value and "skip if
+# default" functions ourselves.
+#
+# There are three cases:
+#
+# 1. Default is the same as the Rust default for the type
+# In that case, the field becomes:
+#
+# #[serde(default, skip_serializing_if = "crate::is_default")]
+# nameoffield: bool
+#
+# 2. Type is primitive and default value is different from the Rust default, e.g.:
+#
+# #[serde(default = "crate::U64::<2000>::value",
+#     skip_serializing_if = "crate::U64::<2000>::is_equal")]
+# nameoffield: u64
+#
+# U64<constant> is implemented in rust/src/helpers.rs
+#
+# 3. Type is not primitive and has default value different from Rust default, e.g.:
+#
+# [serde(default = "crate::default_value_incoming_allow_from",
+#     skip_serializing_if = "crate::default_value_equal_incoming_allow_from")]
+# allow_from: Vec<String>
+#
+# The two functions default_value_incoming_allow_from() and
+# default_value_equal_incoming_allow_from() are also generated.
+#
+from enum import Enum
+from enum import auto
+import os
+import re
+import sys
+
+class LType(Enum):
+    """The type we handle in table.py"""
+    Bool = auto()
+    Command = auto()
+    Double = auto()
+    ListAuthZones = auto()
+    ListSocketAddresses = auto()
+    ListStrings = auto()
+    ListSubnets = auto()
+    ListForwardZones = auto()
+    String = auto()
+    Uint64 = auto()
+
+def get_olddoc_typename(typ):
+    """Given a type from table.py, return the old-style type name"""
+    if typ == LType.Bool:
+        return 'Boolean'
+    if typ == LType.Uint64:
+        return 'Integer'
+    if typ == LType.Double:
+        return 'Double'
+    if typ == LType.String:
+        return 'String'
+    if typ == LType.ListSocketAddresses:
+        return 'Comma separated list or IPs of IP:port combinations'
+    if typ == LType.ListSubnets:
+        return 'Comma separated list of IP addresses or subnets, negation supported'
+    if typ == LType.ListStrings:
+        return 'Comma separated list of strings'
+    if typ == LType.ListForwardZones:
+        return 'Comma separated list of \'zonename=IP\' pairs'
+    if typ == LType.ListAuthZones:
+        return 'Comma separated list of \'zonename=filename\' pairs'
+    return 'Unknown' + str(typ)
+
+def get_newdoc_typename(typ):
+    """Given a type from table.py, return the new-style type name"""
+    if typ == LType.Bool:
+        return 'Boolean'
+    if typ == LType.Uint64:
+        return 'Integer'
+    if typ == LType.Double:
+        return 'Double'
+    if typ == LType.String:
+        return 'String'
+    if typ == LType.ListSocketAddresses:
+        return 'Sequence of `Socket Address`_ (IP or IP:port combinations)'
+    if typ == LType.ListSubnets:
+        return 'Sequence of `Subnet`_ (IP addresses or subnets, negation supported)'
+    if typ == LType.ListStrings:
+        return 'Sequence of strings'
+    if typ == LType.ListForwardZones:
+        return 'Sequence of `Forward Zone`_'
+    if typ == LType.ListAuthZones:
+        return 'Sequence of `Auth Zone`_'
+    return 'Unknown' + str(typ)
+
+def get_default_olddoc_value(typ, val):
+    """Given a type and a value from table.py return the old doc representation of the value"""
+    if typ == LType.Bool:
+        if val == 'false':
+            return 'no'
+        return 'yes'
+    if val == '':
+        return '(empty)'
+    return val
+
+def get_default_newdoc_value(typ, val):
+    """Given a type and a value from table.py return the new doc representation of the value"""
+    if typ in (LType.Bool, LType.Uint64, LType.Double):
+        return '``' + val + '``'
+    if typ == LType.String and val == '':
+        return '(empty)'
+    if typ == LType.String:
+        return '``' + val + '``'
+    if val == '':
+        return '``[]``'
+    return '``[' + val + ']``'
+
+def get_rust_type(typ):
+    """Determine which Rust type is used for a logical type"""
+    if typ == LType.Bool:
+        return 'bool'
+    if typ == LType.Uint64:
+        return 'u64'
+    if typ == LType.Double:
+        return 'f64'
+    if typ == LType.String:
+        return 'String'
+    if typ == LType.ListSocketAddresses:
+        return 'Vec<String>'
+    if typ == LType.ListSubnets:
+        return 'Vec<String>'
+    if typ == LType.ListStrings:
+        return 'Vec<String>'
+    if typ == LType.ListForwardZones:
+        return 'Vec<ForwardZone>'
+    if typ == LType.ListAuthZones:
+        return 'Vec<AuthZone>'
+    return 'Unknown' + str(typ)
+
+def quote(arg):
+    """Return a quoted string"""
+    return '"' + arg + '"'
+
+def gen_cxx_defineoldsettings(file, entries):
+    """Generate C++ code to declare old-style settings"""
+    file.write('void pdns::settings::rec::defineOldStyleSettings()\n{\n')
+    for entry in entries:
+        helptxt = quote(entry['help'])
+        oldname = quote(entry['oldname'])
+        if entry['type'] == LType.Bool:
+            if entry['default'] == "true":
+                cxxdef = "yes"
+            else:
+                cxxdef = "no"
+            cxxdef = quote(cxxdef)
+            file.write(f"  ::arg().setSwitch({oldname}, {helptxt}) = {cxxdef};\n")
+        elif entry['type'] == LType.Command:
+            file.write(f"  ::arg().setCmd({oldname}, {helptxt});\n")
+        else:
+            file.write(f"  ::arg().set({oldname}, {helptxt}) = {quote(entry['default'])};\n")
+    file.write('}\n\n')
+
+def gen_cxx_oldstylesettingstobridgestruct(file, entries):
+    """Generate C++ code the convert old-style settings to new-style struct"""
+    file.write('void pdns::settings::rec::oldStyleSettingsToBridgeStruct')
+    file.write('(Recursorsettings& settings)\n{\n')
+    for entry in entries:
+        if entry['type'] == LType.Command:
+            continue
+        if 'skip-yaml' in entry:
+            continue
+        rust_type = get_rust_type(entry['type'])
+        name = entry['name']
+        oldname = entry['oldname']
+        section = entry['section']
+        file.write(f'  settings.{section}.{name} = ')
+        if rust_type == 'bool':
+            file.write(f'arg().mustDo("{oldname}")')
+        elif rust_type == 'u64':
+            file.write(f'static_cast<uint64_t>(arg().asNum("{oldname}"))')
+        elif rust_type == 'f64':
+            file.write(f'arg().asDouble("{oldname}")')
+        elif rust_type == 'String':
+            file.write(f'arg()["{oldname}"]')
+        elif rust_type == 'Vec<String>':
+            file.write(f'getStrings("{oldname}")')
+        elif rust_type == 'Vec<ForwardZone>':
+            file.write(f'getForwardZones("{oldname}")')
+        elif rust_type == 'Vec<AuthZone>':
+            file.write(f'getAuthZones("{oldname}")')
+        else:
+            file.write(f'Unknown type {rust_type}\n')
+        file.write(';\n')
+    file.write('}\n\n')
+
+def gen_cxx_oldkvtobridgestruct(file, entries):
+    """Generate C++ code for oldKVToBridgeStruct"""
+    file.write('// Inefficient, but only meant to be used for one-time conversion purposes\n')
+    file.write('bool pdns::settings::rec::oldKVToBridgeStruct(std::string& key, ')
+    file.write('const std::string& value, ::rust::String& section, ::rust::String& fieldname, ')
+    file.write('::rust::String& type_name, pdns::rust::settings::rec::Value& rustvalue)')
+    file.write('{ // NOLINT(readability-function-cognitive-complexity)\n')
+    file.write('  if (const auto newname = arg().isDeprecated(key); !newname.empty()) {\n')
+    file.write('    key = newname;\n')
+    file.write('  }\n')
+    for entry in entries:
+        if entry['type'] == LType.Command:
+            continue
+        if 'skip-yaml' in entry:
+            continue
+        rust_type = get_rust_type(entry['type'])
+        extra = ''
+        if entry['oldname'] == 'forward-zones-recurse':
+            extra = ', true'
+        name = entry['name']
+        section = entry['section']
+        oldname = entry['oldname']
+        file.write(f'  if (key == "{oldname}") {{\n')
+        file.write(f'    section = "{section}";\n')
+        file.write(f'    fieldname = "{name}";\n')
+        file.write(f'    type_name = "{rust_type}";\n')
+        if rust_type == 'bool':
+            file.write(f'    to_yaml(rustvalue.bool_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        elif rust_type == 'u64':
+            file.write(f'    to_yaml(rustvalue.u64_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        elif rust_type == 'f64':
+            file.write(f'    to_yaml(rustvalue.f64_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        elif rust_type == 'String':
+            file.write(f'    to_yaml(rustvalue.string_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        elif rust_type == 'Vec<String>':
+            file.write(f'    to_yaml(rustvalue.vec_string_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        elif rust_type == 'Vec<ForwardZone>':
+            file.write(f'    to_yaml(rustvalue.vec_forwardzone_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        elif rust_type == 'Vec<AuthZone>':
+            file.write(f'    to_yaml(rustvalue.vec_authzone_val, value{extra});\n')
+            file.write('    return true;\n  }\n')
+        else:
+            file.write(f'Unknown type {rust_type}\n')
+    file.write('  return false;\n')
+    file.write('}\n\n')
+
+def gen_cxx_brigestructtoldstylesettings(file, entries):
+    """Generate C++ Code for bridgeStructToOldStyleSettings"""
+    file.write('void pdns::settings::rec::bridgeStructToOldStyleSettings')
+    file.write('(const Recursorsettings& settings)\n{\n')
+    for entry in entries:
+        if entry['type'] == LType.Command:
+            continue
+        if 'skip-yaml' in entry:
+            continue
+        section = entry['section'].lower()
+        name = entry['name']
+        oldname = entry['oldname']
+        file.write(f'  ::arg().set("{oldname}") = ')
+        file.write(f'to_arg(settings.{section}.{name});\n')
+    file.write('}\n')
+
+def gen_cxx(entries):
+    """Generate the C++ code from the defs in table.py"""
+    with open('cxxsettings-generated.cc', mode='w', encoding="UTF-8") as file:
+        file.write('// THIS IS A GENERATED FILE. DO NOT EDIT. SOURCE: see settings dir\n\n')
+        file.write('#include "arguments.hh"\n')
+        file.write('#include "cxxsettings.hh"\n')
+        file.write('#include "cxxsettings-private.hh"\n\n')
+        gen_cxx_defineoldsettings(file, entries)
+        gen_cxx_oldstylesettingstobridgestruct(file, entries)
+        gen_cxx_oldkvtobridgestruct(file, entries)
+        gen_cxx_brigestructtoldstylesettings(file, entries)
+
+def is_value_rust_default(typ, value):
+    """Is a value (represented as string) the same as its corresponding Rust default?"""
+    if typ == 'bool':
+        return value == 'false'
+    if typ == 'u64':
+        return value in ('0', '')
+    if typ == 'f64':
+        return value == '0.0'
+    if typ == 'String':
+        return value == ''
+    return False
+
+def gen_rust_forwardzonevec_default_fucntions(name):
+    """Generate Rust code for the default handling of a vector for ForwardZones"""
+    ret = f'// DEFAULT HANDLING for {name}\n'
+    ret += f'fn default_value_{name}() -> Vec<recsettings::ForwardZone> {{\n'
+    ret += '    Vec::new()\n'
+    ret += '}\n'
+    ret += f'fn default_value_equal_{name}(value: &Vec<recsettings::ForwardZone>)'
+    ret += '-> bool {\n'
+    ret += f'    let def = default_value_{name}();\n'
+    ret += '    &def == value\n'
+    ret += '}\n\n'
+    return ret
+
+def gen_rust_authzonevec_default_functions(name):
+    """Generate Rust code for the default handling of a vector for AuthZones"""
+    ret = f'// DEFAULT HANDLING for {name}\n'
+    ret += f'fn default_value_{name}() -> Vec<recsettings::AuthZone> {{\n'
+    ret += '    Vec::new()\n'
+    ret += '}\n'
+    ret += f'fn default_value_equal_{name}(value: &Vec<recsettings::AuthZone>)'
+    ret += '-> bool {\n'
+    ret += f'    let def = default_value_{name}();\n'
+    ret += '    &def == value\n'
+    ret += '}\n\n'
+    return ret
+
+# Example snippet generated
+# fn default_value_general_query_local_address() -> Vec<String> {
+#    vec![String::from("0.0.0.0"), ]
+#}
+#fn default_value_equal_general_query_local_address(value: &Vec<String>) -> bool {
+#    let def = default_value_general_query_local_address();
+#    &def == value
+#}
+def gen_rust_stringvec_default_functions(entry, name):
+    """Generate Rust code for the default handling of a vector for Strings"""
+    ret = f'// DEFAULT HANDLING for {name}\n'
+    ret += f'fn default_value_{name}() -> Vec<String> {{\n'
+    parts = re.split('[ \t,]+', entry['default'])
+    if len(parts) > 0:
+        ret += '    vec![\n'
+        for part in  parts:
+            if part == '':
+                continue
+            ret += f'        String::from({quote(part)}),\n'
+        ret += '    ]\n'
+    else:
+        ret  += '    vec![]\n'
+    ret += '}\n'
+    ret += f'fn default_value_equal_{name}(value: &Vec<String>) -> bool {{\n'
+    ret += f'    let def = default_value_{name}();\n'
+    ret += '    &def == value\n'
+    ret += '}\n\n'
+    return ret
+
+def gen_rust_default_functions(entry, name, rust_type):
+    """Generate Rust code for the default handling"""
+    if entry['type'] in (LType.ListSocketAddresses, LType.ListSubnets, LType.ListStrings):
+        return gen_rust_stringvec_default_functions(entry, name)
+    if entry['type'] == LType.ListForwardZones:
+        return gen_rust_forwardzonevec_default_fucntions(name)
+    if entry['type'] == LType.ListAuthZones:
+        return gen_rust_authzonevec_default_functions(name)
+    ret = f'// DEFAULT HANDLING for {name}\n'
+    ret += f'fn default_value_{name}() -> {rust_type} {{\n'
+    ret += f"    String::from({quote(entry['default'])})\n"
+    ret += '}\n'
+    if rust_type == 'String':
+        rust_type = 'str'
+    ret += f'fn default_value_equal_{name}(value: &{rust_type})'
+    ret += '-> bool {\n'
+    ret += f'    value == default_value_{name}()\n'
+    ret += '}\n\n'
+    return ret
+
+def write_rust_field(file, entry, default_funcs):
+    """Generate Rust code for a field with Serde annotations"""
+    rust_type = get_rust_type(entry['type'])
+    the_default = entry['default']
+    is_rust_default = is_value_rust_default(rust_type, the_default)
+    if is_rust_default:
+        file.write('        #[serde(default, skip_serializing_if = "crate::is_default")]\n')
+    else:
+        if entry['type'] == LType.Bool:
+            file.write('        #[serde(default = "crate::Bool::<true>::value", ')
+            file.write('skip_serializing_if = "crate::if_true")]\n')
+        elif entry['type'] == LType.Uint64:
+            file.write(f'        #[serde(default = "crate::U64::<{the_default}>::value", ')
+            file.write(f'skip_serializing_if = "crate::U64::<{the_default}>::is_equal")]\n')
+        else:
+            basename = entry['section'] + '_' + entry['name']
+            file.write(f'        #[serde(default = "crate::default_value_{basename}", ')
+            file.write(f'skip_serializing_if = "crate::default_value_equal_{basename}")]\n')
+            default_funcs.append(gen_rust_default_functions(entry, basename, rust_type))
+    file.write(f"        {entry['name']}: {rust_type},\n\n")
+
+def write_rust_section(file, section, entries, default_funcs):
+    """Generate Rust code for a Section with Serde annotations"""
+    file.write(f'    // SECTION {section.capitalize()}\n')
+    file.write('    #[derive(Deserialize, Serialize, Debug, PartialEq)]\n')
+    file.write('    #[serde(deny_unknown_fields)]\n')
+    file.write(f'    pub struct {section.capitalize()} {{\n')
+    for entry in entries:
+        if entry['section'] != section:
+            continue
+        if entry['type'] == LType.Command:
+            continue
+        if 'skip-yaml' in entry:
+            continue
+        write_rust_field(file, entry, default_funcs)
+    file.write(f'    }}\n    // END SECTION {section.capitalize()}\n\n')
+
+#
+# Each section als has a Default implementation, so that a section with all entries having a default
+# value does not get generated into a yaml section. Such a tarit looks like:
+#
+#impl Default for recsettings::ForwardZone {
+#    fn default() -> Self {
+#        let deserialized: recsettings::ForwardZone = serde_yaml::from_str("").unwrap();
+#        deserialized
+#    }
+#}
+
+def write_rust_default_trait_impl(file, section):
+    """Generate Rust code for the default Trait for a section"""
+    file.write(f'impl Default for recsettings::{section.capitalize()} {{\n')
+    file.write('    fn default() -> Self {\n')
+    file.write('        let deserialized: recsettings::')
+    file.write(f'{section.capitalize()} = serde_yaml::from_str("").unwrap();\n')
+    file.write('        deserialized\n')
+    file.write('    }\n')
+    file.write('}\n\n')
+
+def write_validator(file, section, entries):
+    """Generate Rust code for the Validator Trait for a section"""
+    file.write(f'impl Validate for recsettings::{section.capitalize()} {{\n')
+    file.write('    fn validate(&self) -> Result<(), ValidationError> {\n')
+    for entry in entries:
+        if entry['section'] != section:
+            continue
+        name = entry['name'].lower()
+        typ = entry['type']
+        if typ == LType.ListSubnets:
+            validator = 'validate_subnet'
+        elif typ == LType.ListSocketAddresses:
+            validator = 'validate_socket_address'
+        elif typ == LType.ListForwardZones:
+            validator = '|field, element| element.validate(field)'
+        elif typ == LType.ListAuthZones:
+            validator = '|field, element| element.validate(field)'
+        else:
+            continue
+        file.write(f'        let fieldname = "{section.lower()}.{name}".to_string();\n')
+        file.write(f'        validate_vec(&fieldname, &self.{name}, {validator})?;\n')
+    file.write('        Ok(())\n')
+    file.write('    }\n')
+    file.write('}\n\n')
+
+def write_rust_merge_trait_impl(file, section, entries):
+    """Generate Rust code for the Merge Trait for a section"""
+    file.write(f'impl Merge for recsettings::{section.capitalize()} {{\n')
+    file.write('    fn merge(&mut self, rhs: &mut Self, map: Option<&serde_yaml::Mapping>) {\n')
+    file.write('        if let Some(m) = map {\n')
+    for entry in entries:
+        if entry['section'] != section:
+            continue
+        if 'skip-yaml' in entry:
+            continue
+        rtype = get_rust_type(entry['type'])
+        name = entry['name']
+        file.write(f'            if m.contains_key("{name}") {{\n')
+        if rtype in ('bool', 'u64', 'f64', 'String'):
+            file.write(f'                self.{name} = rhs.{name}.to_owned();\n')
+        else:
+            file.write(f'                if is_overriding(m, "{name}") || ')
+            file.write(f'self.{name} == DEFAULT_CONFIG.{section}.{name} {{\n')
+            file.write(f'                    self.{name}.clear();\n')
+            file.write('                }\n')
+            file.write(f'                merge_vec(&mut self.{name}, &mut rhs.{name});\n')
+        file.write('            }\n')
+    file.write('        }\n')
+    file.write('    }\n')
+    file.write('}\n\n')
+
+def gen_rust(entries):
+    """Generate Rust code all entries"""
+    def_functions = []
+    sections = {}
+    with open('rust/src/lib.rs', mode='w', encoding='UTF-8') as file:
+        file.write('// THIS IS A GENERATED FILE. DO NOT EDIT. SOURCE: see settings dir\n')
+        file.write('// START INCLUDE rust-preable-in.rs\n')
+        with open('rust-preamble-in.rs', mode='r', encoding='UTF-8') as pre:
+            file.write(pre.read())
+            file.write('// END INCLUDE rust-preamble-in.rs\n\n')
+
+        file.write('#[cxx::bridge(namespace = "pdns::rust::settings::rec")]\n')
+        file.write('mod recsettings {\n')
+        with open('rust-bridge-in.rs', mode='r', encoding='UTF-8') as bridge:
+            file.write('    // START INCLUDE rust-bridge-in.rs\n')
+            for line in bridge:
+                file.write('    ' + line)
+
+        file.write('    // END INCLUDE rust-bridge-in.rs\n\n')
+        for entry in entries:
+            if entry['section'] == 'commands':
+                continue
+            sections[entry['section']] = entry['section']
+
+        for section in sections:
+            write_rust_section(file, section, entries, def_functions)
+
+        file.write('    #[derive(Serialize, Deserialize, Debug)]\n')
+        file.write('    #[serde(deny_unknown_fields)]\n')
+        file.write('    pub struct Recursorsettings {\n')
+        for section in sections:
+            file.write('        #[serde(default, skip_serializing_if = "crate::is_default")]\n')
+            file.write(f'        {section.lower()}: {section.capitalize()},\n')
+        file.write('}  // End of generated structs\n')
+        file.write('}\n')
+
+        for section in sections:
+            write_rust_default_trait_impl(file, section)
+        write_rust_default_trait_impl(file, 'Recursorsettings')
+
+        for section in sections:
+            write_validator(file, section, entries)
+
+        file.write('impl crate::recsettings::Recursorsettings {\n')
+        file.write('    fn validate(&self) -> Result<(), ValidationError> {\n')
+        for section in sections:
+            file.write(f'        self.{section.lower()}.validate()?;\n')
+        file.write('        Ok(())\n')
+        file.write('    }\n')
+        file.write('}\n\n')
+
+        for section in sections:
+            write_rust_merge_trait_impl(file, section, entries)
+
+        file.write('impl Merge for crate::recsettings::Recursorsettings {\n')
+        file.write('    fn merge(&mut self, rhs: &mut Self, map: Option<&serde_yaml::Mapping>) {\n')
+        file.write('        if let Some(m) = map {\n')
+        for section in sections:
+            file.write(f'            if let Some(s) = m.get("{section}") {{\n')
+            file.write('                if s.is_mapping() {\n')
+            file.write((f'                    self.{section}.merge(&mut rhs.{section},'
+                       ' s.as_mapping());\n'))
+            file.write('                }\n')
+            file.write('            }\n')
+        file.write('        }\n')
+        file.write('    }\n')
+        file.write('}\n\n')
+
+        for entry in def_functions:
+            file.write(entry)
+        file.close()
+
+def gen_docs_meta(file, entry, name, is_tuple):
+    """Write .. versionadded:: and related entries"""
+    if name in entry:
+        val = entry[name]
+        if not isinstance(val, list):
+            val = [val]
+        for vers in val:
+            if is_tuple:
+                file.write(f'.. {name}:: {vers[0]}\n\n')
+                file.write(f'  {vers[1].strip()}\n')
+            else:
+                file.write(f'.. {name}:: {vers}\n')
+
+def gen_oldstyle_docs(entries):
+    """Write old style docs"""
+    with open('../docs/settings.rst', mode='w', encoding='UTF-8') as file:
+        file.write('.. THIS IS A GENERATED FILE. DO NOT EDIT. SOURCE: see settings dir\n')
+        file.write('   START INCLUDE docs-old-preamble-in.rst\n\n')
+        with open('docs-old-preamble-in.rst', mode='r', encoding='UTF-8') as pre:
+            file.write(pre.read())
+            file.write('.. END INCLUDE docs-old-preamble-in.rst\n\n')
+
+        for entry in entries:
+            if entry['type'] == LType.Command:
+                continue
+            if entry['doc'].strip() == 'SKIP':
+                continue
+            oldname = entry['oldname']
+            section = entry['section']
+            file.write(f'.. _setting-{oldname}:\n\n')
+            file.write(f'``{oldname}``\n')
+            dots = '~' * (len(entry['oldname']) + 4)
+            file.write(f'{dots}\n')
+            gen_docs_meta(file, entry, 'versionadded', False)
+            gen_docs_meta(file, entry, 'versionchanged', True)
+            gen_docs_meta(file, entry, 'deprecated', True)
+            if 'doc-rst' in entry:
+                file.write(entry['doc-rst'].strip())
+                file.write('\n')
+            file.write('\n')
+            typ = get_olddoc_typename(entry['type'])
+            file.write(f'-  {typ}\n')
+            if 'docdefault' in entry:
+                file.write(f"-  Default: {entry['docdefault']}\n\n")
+            else:
+                file.write((f"-  Default: "
+                            f"{get_default_olddoc_value(entry['type'], entry['default'])}\n\n"))
+            if 'skip-yaml' in entry:
+                file.write('- YAML setting does not exist\n\n')
+            else:
+                file.write(f"- YAML setting: :ref:`setting-yaml-{section}.{entry['name']}`\n\n")
+            file.write(entry['doc'].strip())
+            file.write('\n\n')
+
+def fixxrefs(entries, arg):
+    """Docs in table refer to old style names, we modify them to ref to new style"""
+    matches = re.findall(':ref:`setting-(.*?)`', arg)
+    # We want to replace longest match first, to avoid a short match modifying a long one
+    matches = sorted(matches, key = lambda x: -len(x))
+    for match in matches:
+        for entry in entries:
+            if entry['oldname'] == match:
+                key = ':ref:`setting-' + match
+                repl = ':ref:`setting-yaml-' + entry['section'] + '.' + entry['name']
+                arg = arg.replace(key, repl)
+    return arg
+
+def gen_newstyle_docs(argentries):
+    """Write new style docs"""
+    entries = sorted(argentries, key = lambda entry: [entry['section'], entry['name']])
+    with open('../docs/yamlsettings.rst', 'w', encoding='utf-8') as file:
+        file.write('.. THIS IS A GENERATED FILE. DO NOT EDIT. SOURCE: see settings dir\n')
+        file.write('   START INCLUDE docs-new-preamble-in.rst\n\n')
+        with open('docs-new-preamble-in.rst', mode='r', encoding='utf-8') as pre:
+            file.write(pre.read())
+            file.write('.. END INCLUDE docs-new-preamble-in.rst\n\n')
+
+        for entry in entries:
+            if entry['type'] == LType.Command:
+                continue
+            if entry['doc'].strip() == 'SKIP':
+                continue
+            if 'skip-yaml' in entry:
+                continue
+            section = entry['section']
+            name = entry['name']
+            fullname = section + '.' + name
+            file.write(f'.. _setting-yaml-{fullname}:\n\n')
+            file.write(f'``{fullname}``\n')
+            dots = '^' * (len(fullname) + 4)
+            file.write(f'{dots}\n')
+            gen_docs_meta(file, entry, 'versionadded', False)
+            gen_docs_meta(file, entry, 'versionchanged', True)
+            gen_docs_meta(file, entry, 'deprecated', True)
+            if 'doc-rst' in entry:
+                file.write(fixxrefs(entries, entry['doc-rst'].strip()))
+                file.write('\n')
+            file.write('\n')
+            file.write(f"-  {get_newdoc_typename(entry['type'])}\n")
+            if 'docdefault' in entry:
+                file.write(f"-  Default: {entry['docdefault']}\n\n")
+            else:
+                file.write((f"-  Default: "
+                            f"{get_default_newdoc_value(entry['type'], entry['default'])}\n\n"))
+            file.write(f"- Old style setting: :ref:`setting-{entry['oldname']}`\n\n")
+            if 'doc-new' in entry:
+                file.write(fixxrefs(entries, entry['doc-new'].strip()))
+            else:
+                file.write(fixxrefs(entries, entry['doc'].strip()))
+            file.write('\n\n')
+
+RUNTIME = '*runtime determined*'
+
+def generate():
+    """Read table, validate and generate C++, Rst and .rst files"""
+    # read table
+    with open('table.py', mode='r', encoding="utf-8") as file:
+        entries = eval(file.read())
+
+    for entry in entries:
+        the_oldname = entry['name'].replace('_', '-')
+        if 'oldname' in entry:
+            if entry['oldname'] == the_oldname:
+                sys.stderr.write(f"Redundant old name {entry['oldname']}\n")
+        else:
+            entry['oldname'] = the_oldname
+
+    dupcheck1 = {}
+    dupcheck2 = {}
+    for entry in entries:
+        if entry['oldname'] in dupcheck1:
+            sys.stderr.write(f"duplicate entries with oldname = {entry['oldname']}\n")
+            sys.exit(1)
+        if entry['section'] + '.' + entry['name'] in dupcheck2:
+            sys.stderr.write((f"duplicate entries with section.name = "
+                              f"{entry['section']}.{ entry['name']}\n"))
+            sys.exit(1)
+        dupcheck1[entry['oldname']] = True
+        dupcheck2[entry['section'] + '.' + entry['name']] = True
+    # And generate C++, Rust and docs code based on table
+    gen_cxx(entries)
+    gen_rust(entries)
+    # Avoid generating doc files in a sdist based build
+    if os.path.isdir('../docs'):
+        gen_oldstyle_docs(entries)
+        gen_newstyle_docs(entries)
+
+generate()
diff --git a/pdns/recursordist/settings/rust-bridge-in.rs b/pdns/recursordist/settings/rust-bridge-in.rs
new file mode 100644 (file)
index 0000000..1ee664c
--- /dev/null
@@ -0,0 +1,109 @@
+// This file (rust-bridge-in.rs) is included into lib.rs inside the bridge module
+/*
+ * Implement non-generated structs that need to be handled by Serde and CXX
+ */
+
+// A single forward zone
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+#[serde(deny_unknown_fields)]
+pub struct ForwardZone {
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    zone: String,
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    forwarders: Vec<String>,
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    recurse: bool,
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    notify_allowed: bool,
+}
+
+// A single auth zone
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+#[serde(deny_unknown_fields)]
+pub struct AuthZone {
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    zone: String,
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    file: String,
+}
+
+// A struct holding bot a vector of forward zones and a vector o auth zones, used by REST API code
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+#[serde(deny_unknown_fields)]
+struct ApiZones {
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    auth_zones: Vec<AuthZone>,
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    forward_zones: Vec<ForwardZone>,
+}
+
+// Two structs used to generated YAML based on a vector of name to value mappings
+// Cannot use Enum as CXX has only very basic Enum support
+struct Value {
+    bool_val: bool,
+    u64_val: u64,
+    f64_val: f64,
+    string_val: String,
+    vec_string_val: Vec<String>,
+    vec_forwardzone_val: Vec<ForwardZone>,
+    vec_authzone_val: Vec<AuthZone>,
+}
+
+struct OldStyle {
+    section: String,
+    name: String,
+    old_name: String,
+    type_name: String,
+    value: Value,
+    overriding: bool,
+}
+
+/*
+ * Functions callable from C++
+ */
+extern "Rust" {
+    // Parse a string representing YAML text and produce the corresponding data structure known to Serde
+    // The settings that can be stored in individual files get their own parse function
+    // Main recursor settings
+    fn parse_yaml_string(str: &str) -> Result<Recursorsettings>;
+    // Allow from sequence
+    fn parse_yaml_string_to_allow_from(str: &str) -> Result<Vec<String>>;
+    // Forward zones sequence
+    fn parse_yaml_string_to_forward_zones(str: &str) -> Result<Vec<ForwardZone>>;
+    // Allow notify for sequence
+    fn parse_yaml_string_to_allow_notify_for(str: &str) -> Result<Vec<String>>;
+    // REST APIU zones
+    fn parse_yaml_string_to_api_zones(str: &str) -> Result<ApiZones>;
+
+    // Prdoduce a YAML formatted sting given a data structure known to Serde
+    fn to_yaml_string(self: &Recursorsettings) -> Result<String>;
+    // When doing a conversion of old-style to YAML style we use a vector of OldStyle structs
+    fn map_to_yaml_string(map: &Vec<OldStyle>) -> Result<String>;
+    fn forward_zones_to_yaml_string(vec: &Vec<ForwardZone>) -> Result<String>;
+    fn allow_from_to_yaml_string(vec: &Vec<String>) -> Result<String>;
+    fn allow_from_to_yaml_string_incoming(key: &String, filekey: &String, vec: &Vec<String>) -> Result<String>;
+    fn allow_for_to_yaml_string(vec: &Vec<String>) -> Result<String>;
+
+    // Merge a string representing YAML settings into a existing setttings struct
+    fn merge(lhs: &mut Recursorsettings, rhs: &str) -> Result<()>;
+
+    // Validate the sections inside the main settings struct, sections themselves will valdiate their fields
+    fn validate(self: &Recursorsettings) -> Result<()>;
+    // The validate function bewlo are "hand-crafted" as their structs afre mnot generated
+    fn validate(self: &AuthZone, field: &str) -> Result<()>;
+    fn validate(self: &ForwardZone, field: &str) -> Result<()>;
+    fn validate(self: &ApiZones, field: &str) -> Result<()>;
+
+    // Helper functions to call the proper validate function on vectors of various kinds
+    fn validate_auth_zones(field: &str, vec: &Vec<AuthZone>) -> Result<()>;
+    fn validate_forward_zones(field: &str, vec: &Vec<ForwardZone>) -> Result<()>;
+    fn validate_allow_for(field: &str, vec: &Vec<String>) -> Result<()>;
+    fn validate_allow_notify_for(field: &str, vec: &Vec<String>) -> Result<()>;
+    fn validate_allow_from(field: &str, vec: &Vec<String>) -> Result<()>;
+
+    // The functions to maintain REST API managed zones
+    fn api_read_zones(path: &str) ->  Result<UniquePtr<ApiZones>>;
+    fn api_add_auth_zone(file: &str, authzone: AuthZone) -> Result<()>;
+    fn api_add_forward_zone(file: &str, forwardzone: ForwardZone) -> Result<()>;
+    fn api_delete_zone(file: &str, zone: &str) -> Result<()>;
+}
diff --git a/pdns/recursordist/settings/rust-preamble-in.rs b/pdns/recursordist/settings/rust-preamble-in.rs
new file mode 100644 (file)
index 0000000..64fef08
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+// This file (rust-preamble-in.rs) is included at the start of lib.rs
+
+use serde::{Deserialize, Serialize};
+
+mod helpers;
+use helpers::*;
+
+mod bridge;
+use bridge::*;
+
+// Suppresses "Deserialize unused" warning
+#[derive(Deserialize, Serialize)]
+struct UnusedStruct {}
+
+trait Validate {
+    fn validate(&self) -> Result<(), ValidationError>;
+}
+
+#[derive(Debug)]
+pub struct ValidationError {
+    msg: String,
+}
+
+trait Merge {
+    fn merge(&mut self, rhs: &mut Self, map: Option<&serde_yaml::Mapping>);
+}
diff --git a/pdns/recursordist/settings/rust/.gitignore b/pdns/recursordist/settings/rust/.gitignore
new file mode 100644 (file)
index 0000000..7b8194b
--- /dev/null
@@ -0,0 +1,6 @@
+/target
+/Makefile
+/Makefile.in
+/cxx.h
+/lib.rs.h
+src/lib.rs
diff --git a/pdns/recursordist/settings/rust/Cargo.lock b/pdns/recursordist/settings/rust/Cargo.lock
new file mode 100644 (file)
index 0000000..d149f04
--- /dev/null
@@ -0,0 +1,265 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe98ba1789d56fb3db3bee5e032774d4f421b685de7ba703643584ba24effbe"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4ce20f6b8433da4841b1dadfb9468709868022d829d5ca1f2ffbda928455ea3"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20888d9e1d2298e2ff473cee30efe7d5036e437857ab68bbfea84c74dba91da2"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fa16a70dd58129e4dfffdff535fb1bce66673f7bbeec4a5a1765a504e1ccd84"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "scratch"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
+
+[[package]]
+name = "serde"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "settings"
+version = "0.1.0"
+dependencies = [
+ "cxx",
+ "cxx-build",
+ "ipnet",
+ "once_cell",
+ "serde",
+ "serde_yaml",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/pdns/recursordist/settings/rust/Cargo.toml b/pdns/recursordist/settings/rust/Cargo.toml
new file mode 100644 (file)
index 0000000..89cef97
--- /dev/null
@@ -0,0 +1,19 @@
+[package]
+name = "settings"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "settings"
+crate-type = ["staticlib"]
+
+[dependencies]
+cxx = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_yaml = "0.9"
+ipnet = "2.8"
+once_cell = "1.18.0"
+
+[build-dependencies]
+cxx-build = "1.0"
+
diff --git a/pdns/recursordist/settings/rust/Makefile.am b/pdns/recursordist/settings/rust/Makefile.am
new file mode 100644 (file)
index 0000000..814c213
--- /dev/null
@@ -0,0 +1,20 @@
+CARGO ?= cargo
+
+all: libsettings.a
+
+EXTRA_DIST = \
+       Cargo.toml \
+       Cargo.lock \
+       build.rs \
+       src/bridge.rs \
+       src/helpers.rs
+
+# should actually end up in a target specific dir...
+libsettings.a lib.rs.h: src/bridge.rs src/lib.rs src/helpers.rs Cargo.toml Cargo.lock build.rs
+       $(CARGO) build --release $(RUST_TARGET)
+       cp target/$(RUSTC_TARGET_ARCH)/release/libsettings.a libsettings.a
+       cp target/$(RUSTC_TARGET_ARCH)/cxxbridge/settings/src/lib.rs.h lib.rs.h
+       cp target/$(RUSTC_TARGET_ARCH)/cxxbridge/rust/cxx.h cxx.h
+
+clean-local:
+       rm -rf libsettings.a src/lib.rs lib.rs.h cxx.h target
diff --git a/pdns/recursordist/settings/rust/build.rs b/pdns/recursordist/settings/rust/build.rs
new file mode 100644 (file)
index 0000000..3bf2cbc
--- /dev/null
@@ -0,0 +1,6 @@
+fn main() {
+    cxx_build::bridge("src/lib.rs")
+        // .file("src/source.cc") at the moment no C++ code callable from Rust
+        .flag_if_supported("-std=c++17")
+        .compile("settings");
+}
diff --git a/pdns/recursordist/settings/rust/src/bridge.rs b/pdns/recursordist/settings/rust/src/bridge.rs
new file mode 100644 (file)
index 0000000..80d1589
--- /dev/null
@@ -0,0 +1,471 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+use once_cell::sync::Lazy;
+use std::fs::File;
+use std::io::{BufReader, BufWriter, ErrorKind, Write};
+use std::net::{IpAddr, SocketAddr};
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use crate::helpers::OVERRIDE_TAG;
+use crate::recsettings::*;
+use crate::{Merge, ValidationError};
+
+impl Default for ForwardZone {
+    fn default() -> Self {
+        let deserialized: ForwardZone = serde_yaml::from_str("").unwrap();
+        deserialized
+    }
+}
+
+impl Default for AuthZone {
+    fn default() -> Self {
+        let deserialized: AuthZone = serde_yaml::from_str("").unwrap();
+        deserialized
+    }
+}
+
+impl Default for ApiZones {
+    fn default() -> Self {
+        let deserialized: ApiZones = serde_yaml::from_str("").unwrap();
+        deserialized
+    }
+}
+
+pub fn validate_socket_address(field: &str, val: &String) -> Result<(), ValidationError> {
+    let sa = SocketAddr::from_str(val);
+    if sa.is_err() {
+        let ip = IpAddr::from_str(val);
+        if ip.is_err() {
+            let msg = format!(
+                "{}: value `{}' is not an IP or IP:port combination",
+                field, val
+            );
+            return Err(ValidationError { msg });
+        }
+    }
+    Ok(())
+}
+
+fn validate_name(field: &str, val: &String) -> Result<(), ValidationError> {
+    if val.is_empty() {
+        let msg = format!("{}: value may not be empty", field);
+        return Err(ValidationError { msg });
+    }
+    if val == "." {
+        return Ok(());
+    }
+    // Strip potential dot at end
+    let mut testval = val.as_str();
+    if testval.ends_with('.') {
+        testval = &testval[0..testval.len() - 1];
+    }
+    for label in testval.split('.') {
+        if label.is_empty() {
+            let msg = format!("{}: `{}' has empty label", field, val);
+            return Err(ValidationError { msg });
+        }
+        // XXX Too liberal, should check for alnum, - and proper \ddd
+        if !label.chars().all(|ch| ch.is_ascii()) {
+            let msg = format!("{}: `{}' contains non-ascii character", field, val);
+            return Err(ValidationError { msg });
+        }
+    }
+    Ok(())
+}
+
+pub fn validate_subnet(field: &str, val: &String) -> Result<(), ValidationError> {
+    if val.is_empty() {
+        let msg = format!("{}: value `{}' is not a subnet or IP", field, val);
+        return Err(ValidationError { msg });
+    }
+    let mut ip = val.as_str();
+    if val.starts_with('!') {
+        ip = &ip[1..];
+    }
+    let subnet = ipnet::IpNet::from_str(ip);
+    if subnet.is_err() {
+        let ip = IpAddr::from_str(ip);
+        if ip.is_err() {
+            let msg = format!("{}: value `{}' is not a subnet or IP", field, val);
+            return Err(ValidationError { msg });
+        }
+    }
+    Ok(())
+}
+
+pub fn validate_vec<T, F>(field: &str, vec: &[T], func: F) -> Result<(), ValidationError>
+where
+    F: Fn(&str, &T) -> Result<(), ValidationError>,
+{
+    vec.iter().try_for_each(|element| func(field, element))
+}
+
+pub fn parse_yaml_string(str: &str) -> Result<Recursorsettings, serde_yaml::Error> {
+    serde_yaml::from_str(str)
+}
+
+pub fn parse_yaml_string_to_allow_from(str: &str) -> Result<Vec<String>, serde_yaml::Error> {
+    serde_yaml::from_str(str)
+}
+
+pub fn parse_yaml_string_to_forward_zones(
+    str: &str,
+) -> Result<Vec<ForwardZone>, serde_yaml::Error> {
+    serde_yaml::from_str(str)
+}
+
+pub fn parse_yaml_string_to_api_zones(str: &str) -> Result<ApiZones, serde_yaml::Error> {
+    serde_yaml::from_str(str)
+}
+
+pub fn parse_yaml_string_to_allow_notify_for(
+    str: &str,
+) -> Result<Vec<String>, serde_yaml::Error> {
+    serde_yaml::from_str(str)
+}
+
+pub fn forward_zones_to_yaml_string(vec: &Vec<ForwardZone>) -> Result<String, serde_yaml::Error> {
+    serde_yaml::to_string(vec)
+}
+
+impl ForwardZone {
+    pub fn validate(&self, field: &str) -> Result<(), ValidationError> {
+        validate_name(&(field.to_owned() + ".zone"), &self.zone)?;
+        if self.forwarders.is_empty() {
+            let msg = format!("{}.forwarders cannot be empty", field);
+            return Err(ValidationError { msg });
+        }
+        validate_vec(
+            &(field.to_owned() + ".forwarders"),
+            &self.forwarders,
+            validate_socket_address,
+        )
+    }
+
+    fn to_yaml_map(&self) -> serde_yaml::Value {
+        let mut seq = serde_yaml::Sequence::new();
+        for entry in &self.forwarders {
+            seq.push(serde_yaml::Value::String(entry.to_owned()));
+        }
+
+        let mut map = serde_yaml::Mapping::new();
+        map.insert(
+            serde_yaml::Value::String("zone".to_owned()),
+            serde_yaml::Value::String(self.zone.to_owned()),
+        );
+        map.insert(
+            serde_yaml::Value::String("recurse".to_owned()),
+            serde_yaml::Value::Bool(self.recurse),
+        );
+        map.insert(
+            serde_yaml::Value::String("forwarders".to_owned()),
+            serde_yaml::Value::Sequence(seq),
+        );
+        serde_yaml::Value::Mapping(map)
+    }
+}
+
+impl AuthZone {
+    pub fn validate(&self, field: &str) -> Result<(), ValidationError> {
+        validate_name(&(field.to_owned() + ".zone"), &self.zone)?;
+        if self.file.is_empty() {
+            let msg = format!("{}.file cannot be empty", field);
+            return Err(ValidationError { msg });
+        }
+        Ok(())
+    }
+
+    fn to_yaml_map(&self) -> serde_yaml::Value {
+        let mut map = serde_yaml::Mapping::new();
+        map.insert(
+            serde_yaml::Value::String("zone".to_owned()),
+            serde_yaml::Value::String(self.zone.to_owned()),
+        );
+        map.insert(
+            serde_yaml::Value::String("file".to_owned()),
+            serde_yaml::Value::String(self.file.to_owned()),
+        );
+        serde_yaml::Value::Mapping(map)
+    }
+}
+
+pub fn validate_auth_zones(field: &str, vec: &Vec<AuthZone>) -> Result<(), ValidationError> {
+    validate_vec(field, vec, |field, element| element.validate(field))
+}
+
+pub fn validate_forward_zones(
+    field: &str,
+    vec: &Vec<ForwardZone>,
+) -> Result<(), ValidationError> {
+    validate_vec(field, vec, |field, element| element.validate(field))
+}
+
+pub fn allow_from_to_yaml_string(vec: &Vec<String>) -> Result<String, serde_yaml::Error> {
+    let mut seq = serde_yaml::Sequence::new();
+    for entry in vec {
+        seq.push(serde_yaml::Value::String(entry.to_owned()));
+    }
+    let val = serde_yaml::Value::Sequence(seq);
+
+    serde_yaml::to_string(&val)
+}
+
+pub fn allow_from_to_yaml_string_incoming(
+    key: &String,
+    filekey: &String,
+    vec: &Vec<String>,
+) -> Result<String, serde_yaml::Error> {
+    // Produce
+    // incoming:
+    //   allow-from-file: ''
+    //   allow-from: !override
+    //    - ...
+    let mut seq = serde_yaml::Sequence::new();
+    for entry in vec {
+        seq.push(serde_yaml::Value::String(entry.to_owned()));
+    }
+
+    let mut innermap = serde_yaml::Mapping::new();
+    innermap.insert(
+        serde_yaml::Value::String(filekey.to_owned()),
+        serde_yaml::Value::String("".to_owned()),
+    );
+    let af = Box::new(serde_yaml::value::TaggedValue {
+        tag: serde_yaml::value::Tag::new(OVERRIDE_TAG),
+        value: serde_yaml::Value::Sequence(seq),
+    });
+    innermap.insert(
+        serde_yaml::Value::String(key.to_owned()),
+        serde_yaml::Value::Tagged(af),
+    );
+
+    let mut outermap = serde_yaml::Mapping::new();
+    outermap.insert(
+        serde_yaml::Value::String("incoming".to_owned()),
+        serde_yaml::Value::Mapping(innermap),
+    );
+    let outerval = serde_yaml::Value::Mapping(outermap);
+
+    serde_yaml::to_string(&outerval)
+}
+
+pub fn validate_allow_from(field: &str, vec: &Vec<String>) -> Result<(), ValidationError> {
+    validate_vec(field, vec, validate_subnet)
+}
+
+pub fn allow_for_to_yaml_string(vec: &Vec<String>) -> Result<String, serde_yaml::Error> {
+    // For purpose of generating yaml allow-for is no different than allow-from as we're handling a
+    // vector of Strings
+    allow_from_to_yaml_string(vec)
+}
+
+pub fn validate_allow_for(field: &str, vec: &Vec<String>) -> Result<(), ValidationError> {
+    validate_vec(field, vec, validate_name)
+}
+
+pub fn validate_allow_notify_for(field: &str, vec: &Vec<String>) -> Result<(), ValidationError> {
+    validate_vec(field, vec, validate_name)
+}
+
+impl Recursorsettings {
+    pub fn to_yaml_string(&self) -> Result<String, serde_yaml::Error> {
+        serde_yaml::to_string(self)
+    }
+
+    // validate() is implemented in the (generated) lib.rs
+}
+
+pub static DEFAULT_CONFIG: Lazy<Recursorsettings> = Lazy::new(Recursorsettings::default);
+
+pub fn merge_vec<T>(lhs: &mut Vec<T>, rhs: &mut Vec<T>) {
+    lhs.append(rhs);
+}
+
+// This is used for conversion, where we want to have !override tags in some cases, so we craft a YAML Mapping by hand
+pub fn map_to_yaml_string(vec: &Vec<OldStyle>) -> Result<String, serde_yaml::Error> {
+    let mut map = serde_yaml::Mapping::new();
+    for entry in vec {
+        let section = entry.section.as_str();
+        if !map.contains_key(section) {
+            let newmap = serde_yaml::Mapping::new();
+            map.insert(
+                serde_yaml::Value::String(section.to_string()),
+                serde_yaml::Value::Mapping(newmap),
+            );
+        }
+        if let Some(mapentry) = map.get_mut(section) {
+            if let Some(mapping) = mapentry.as_mapping_mut() {
+                let val = match entry.type_name.as_str() {
+                    "bool" => serde_yaml::Value::Bool(entry.value.bool_val),
+                    "u64" => {
+                        serde_yaml::Value::Number(serde_yaml::Number::from(entry.value.u64_val))
+                    }
+                    "f64" => {
+                        serde_yaml::Value::Number(serde_yaml::Number::from(entry.value.f64_val))
+                    }
+                    "String" => serde_yaml::Value::String(entry.value.string_val.to_owned()),
+                    "Vec<String>" => {
+                        let mut seq = serde_yaml::Sequence::new();
+                        for element in &entry.value.vec_string_val {
+                            seq.push(serde_yaml::Value::String(element.to_owned()))
+                        }
+                        serde_yaml::Value::Sequence(seq)
+                    }
+                    "Vec<ForwardZone>" => {
+                        let mut seq = serde_yaml::Sequence::new();
+                        for element in &entry.value.vec_forwardzone_val {
+                            seq.push(element.to_yaml_map());
+                        }
+                        serde_yaml::Value::Sequence(seq)
+                    }
+                    "Vec<AuthZone>" => {
+                        let mut seq = serde_yaml::Sequence::new();
+                        for element in &entry.value.vec_authzone_val {
+                            seq.push(element.to_yaml_map());
+                        }
+                        serde_yaml::Value::Sequence(seq)
+                    }
+                    other => serde_yaml::Value::String("map_to_yaml_string: Unknown type: ".to_owned() + other),
+                };
+                if entry.overriding {
+                    let tagged_value = Box::new(serde_yaml::value::TaggedValue {
+                        tag: serde_yaml::value::Tag::new(OVERRIDE_TAG),
+                        value: val,
+                    });
+                    mapping.insert(
+                        serde_yaml::Value::String(entry.name.to_owned()),
+                        serde_yaml::Value::Tagged(tagged_value),
+                    );
+                } else {
+                    mapping.insert(serde_yaml::Value::String(entry.name.to_owned()), val);
+                }
+            }
+        }
+    }
+    serde_yaml::to_string(&map)
+}
+
+pub fn merge(lhs: &mut Recursorsettings, yaml_str: &str) -> Result<(), serde_yaml::Error> {
+    // Parse the yaml for the values
+    let mut rhs: Recursorsettings = serde_yaml::from_str(yaml_str)?;
+    // Parse again for the map containing the keys present, which is used to only override specific values,
+    // taking into account !override tags
+    let map: serde_yaml::Value = serde_yaml::from_str(yaml_str)?;
+    if map.is_mapping() {
+        lhs.merge(&mut rhs, map.as_mapping());
+    }
+
+    Ok(())
+}
+
+// API zones maintenance. In contrast to the old settings code, which creates a settings file per
+// zone, we maintain a single yaml file with all settings. File locking is no issue, as we are the
+// single process managing this dir. So we only use process specific locking.
+
+impl ApiZones {
+    pub fn validate(&self, field: &str) -> Result<(), ValidationError> {
+        validate_auth_zones(&(field.to_owned() + ".auth_zones"), &self.auth_zones)?;
+        validate_forward_zones(&(field.to_owned() + ".forward_zones"), &self.forward_zones)?;
+        Ok(())
+    }
+}
+
+static LOCK: Mutex<bool> = Mutex::new(false);
+
+// Assume we hold the lock
+fn api_read_zones_locked(
+    path: &str,
+    create: bool,
+) -> Result<cxx::UniquePtr<ApiZones>, std::io::Error> {
+    let zones = match File::open(path) {
+        Ok(file) => {
+            let data: Result<ApiZones, serde_yaml::Error> =
+                serde_yaml::from_reader(BufReader::new(file));
+            match data {
+                Err(error) => return Err(std::io::Error::new(ErrorKind::Other, error.to_string())),
+                Ok(yaml) => yaml,
+            }
+        }
+        Err(error) => match error.kind() {
+            // If the file does not exist we return an empty struct
+            ErrorKind::NotFound => {
+                if create {
+                    ApiZones::default()
+                } else {
+                    return Err(error);
+                }
+            }
+            // Any other error is fatal
+            _ => return Err(error),
+        },
+    };
+    Ok(cxx::UniquePtr::new(zones))
+}
+
+// This function is called from C++, it needs to acquire the lock
+pub fn api_read_zones(path: &str) -> Result<cxx::UniquePtr<ApiZones>, std::io::Error> {
+    let _lock = LOCK.lock().unwrap();
+    api_read_zones_locked(path, false)
+}
+
+// Assume we hold the lock
+fn api_write_zones(path: &str, zones: &ApiZones) -> Result<(), std::io::Error> {
+    let mut tmpfile = path.to_owned();
+    tmpfile.push_str(".tmp");
+
+    let file = File::create(tmpfile.as_str())?;
+    let mut buffered_writer = BufWriter::new(&file);
+    if let Err(error) = serde_yaml::to_writer(&mut buffered_writer, &zones) {
+        return Err(std::io::Error::new(ErrorKind::Other, error.to_string()));
+    }
+    buffered_writer.flush()?;
+    file.sync_all()?;
+    drop(buffered_writer);
+    std::fs::rename(tmpfile.as_str(), path)
+}
+
+// This function is called from C++, it needs to acquire the lock
+pub fn api_add_auth_zone(path: &str, authzone: AuthZone) -> Result<(), std::io::Error> {
+    let _lock = LOCK.lock().unwrap();
+    let mut zones = api_read_zones_locked(path, true)?;
+    zones.auth_zones.push(authzone);
+    api_write_zones(path, &zones)
+}
+
+// This function is called from C++, it needs to acquire the lock
+pub fn api_add_forward_zone(path: &str, forwardzone: ForwardZone) -> Result<(), std::io::Error> {
+    let _lock = LOCK.lock().unwrap();
+    let mut zones = api_read_zones_locked(path, true)?;
+    zones.forward_zones.push(forwardzone);
+    api_write_zones(path, &zones)
+}
+// This function is called from C++, it needs to acquire the lock
+pub fn api_delete_zone(path: &str, zone: &str) -> Result<(), std::io::Error> {
+    let _lock = LOCK.lock().unwrap();
+    let mut zones = api_read_zones_locked(path, true)?;
+    zones.auth_zones.retain(|x| x.zone != zone);
+    // Zone data file is unlinked in the C++ caller ws-recursor.cc:doDeleteZone()
+    zones.forward_zones.retain(|x| x.zone != zone);
+    api_write_zones(path, &zones)
+}
diff --git a/pdns/recursordist/settings/rust/src/helpers.rs b/pdns/recursordist/settings/rust/src/helpers.rs
new file mode 100644 (file)
index 0000000..795d8a5
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+use std::{error::Error, fmt};
+use crate::ValidationError;
+
+/* Helper code for validation  */
+impl Error for ValidationError {}
+impl fmt::Display for ValidationError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.msg)
+    }
+}
+
+// Generic helpers
+
+// A helper to define a function returning a constant value and an equal function as a Rust path */
+pub struct U64<const U: u64>;
+impl<const U: u64> U64<U> {
+    pub const fn value() -> u64 {
+        U
+    }
+    pub fn is_equal(v: &u64) -> bool {
+        v == &U
+    }
+}
+
+// A helper to define constant value as a Rust path */
+pub struct Bool<const U: bool>;
+impl<const U: bool> Bool<U> {
+    pub const fn value() -> bool {
+        U
+    }
+}
+
+// A helper used to decide if a bool value should be skipped
+pub fn if_true(v: &bool) -> bool {
+    *v
+}
+
+/* Helper to decide if a value has a default value, as defined by Default trait */
+pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
+    t == &T::default()
+}
+
+pub const OVERRIDE_TAG: &str = "!override";
+
+pub fn is_overriding(m: &serde_yaml::Mapping, key: &str) -> bool{
+    if let Some(serde_yaml::Value::Tagged(vvv)) = m.get(key) {
+        return vvv.tag == OVERRIDE_TAG;
+    }
+    false
+}
+
diff --git a/pdns/recursordist/settings/table.py b/pdns/recursordist/settings/table.py
new file mode 100644 (file)
index 0000000..56ea6c9
--- /dev/null
@@ -0,0 +1,2928 @@
+# This file contains the table used to generate old and new-style settings code
+#
+# Example:
+# {
+# 'name' : 'allow_from',
+# 'section' : 'incoming',
+# 'oldname' : 'allow-from'
+# 'type' : LType.ListSubnets,
+# 'default' : '127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10',
+# 'help' : 'If set, only allow these comma separated netmasks to recurse',
+# 'doc' : '''
+#  '''
+# }
+#
+# See generate.py for a description of the fields.
+#
+# Sections
+# - incoming
+# - outgoing
+# - packetcache
+# - recursor
+# - recordcache
+# - dnssec
+# - webservice
+# - carbon
+# - ecs
+# - logging
+# - nod
+# - snmp
+
+[
+    {
+        'name' : 'aggressive_nsec_cache_size',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '100000',
+        'help' : 'The number of records to cache in the aggressive cache. If set to a value greater than 0, and DNSSEC processing or validation is enabled, the recursor will cache NSEC and NSEC3 records to generate negative answers, as defined in rfc8198',
+        'doc' : '''
+The number of records to cache in the aggressive cache. If set to a value greater than 0, the recursor will cache NSEC and NSEC3 records to generate negative answers, as defined in :rfc:`8198`.
+To use this, DNSSEC processing or validation must be enabled by setting :ref:`setting-dnssec` to ``process``, ``log-fail`` or ``validate``.
+ ''',
+        'versionadded': '4.5.0',
+    },
+    {
+        'name' : 'aggressive_cache_min_nsec3_hit_ratio',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '2000',
+        'help' : 'The minimum expected hit ratio to store NSEC3 records into the aggressive cache',
+        'doc' : '''
+The limit for which to put NSEC3 records into the aggressive cache.
+A value of ``n`` means that an NSEC3 record is only put into the aggressive cache if the estimated probability of a random name hitting the NSEC3 record is higher than ``1/n``.
+A higher ``n`` will cause more records to be put into the aggressive cache, e.g. a value of 4000 will cause records to be put in the aggressive cache even if the estimated probability of hitting them is twice as low as would be the case for ``n=2000``.
+A value of 0 means no NSEC3 records will be put into the aggressive cache.
+
+For large zones the effectiveness of the NSEC3 cache is reduced since each NSEC3 record only covers a randomly distributed subset of all possible names.
+This setting avoids doing unnecessary work for such large zones.
+ ''',
+        'versionadded' : '4.9.0',
+    },
+    {
+        'name' : 'allow_from',
+        'section' : 'incoming',
+        'type' : LType.ListSubnets,
+        'default' : '127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10',
+        'help' : 'If set, only allow these comma separated netmasks to recurse',
+        'doc' : '''
+Netmasks (both IPv4 and IPv6) that are allowed to use the server.
+The default allows access only from :rfc:`1918` private IP addresses.
+An empty value means no checking is done, all clients are allowed.
+Due to the aggressive nature of the internet these days, it is highly recommended to not open up the recursor for the entire internet.
+Questions from IP addresses not listed here are ignored and do not get an answer.
+
+When the Proxy Protocol is enabled (see :ref:`setting-proxy-protocol-from`), the recursor will check the address of the client IP advertised in the Proxy Protocol header instead of the one of the proxy.
+
+Note that specifying an IP address without a netmask uses an implicit netmask of /32 or /128.
+ ''',
+    },
+    {
+        'name' : 'allow_from_file',
+        'section' : 'incoming',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, load allowed netmasks from this file',
+        'doc' : '''
+Like :ref:`setting-allow-from`, except reading from file.
+Overrides the :ref:`setting-allow-from` setting. To use this feature, supply one netmask per line, with optional comments preceded by a '#'.
+ ''',
+        'doc-new' : '''
+Like :ref:`setting-allow-from`, except reading a sequence of `Subnet`_ from file.
+Overrides the :ref:`setting-allow-from` setting. Example content of th specified file:
+
+.. code-block:: yaml
+
+ - 127.0.01
+ - ::1
+
+ ''',
+    },
+    {
+        'name' : 'allow_notify_for',
+        'section' : 'incoming',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'If set, NOTIFY requests for these zones will be allowed',
+        'doc' : '''
+Domain names specified in this list are used to permit incoming
+NOTIFY operations to wipe any cache entries that match the domain
+name. If this list is empty, all NOTIFY operations will be ignored.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'allow_notify_for_file',
+        'section' : 'incoming',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, load NOTIFY-allowed zones from this file',
+        'doc' : '''
+Like :ref:`setting-allow-notify-for`, except reading from file. To use this
+feature, supply one domain name per line, with optional comments
+preceded by a '#'.
+
+NOTIFY-allowed zones can also be specified using :ref:`setting-forward-zones-file`.
+ ''',
+        'doc-new' : '''
+Like :ref:`setting-allow-notify-for`, except reading a sequence of names from file. Example contents of specified file:
+
+.. code-block:: yaml
+
+ - example.com
+ - example.org
+
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'allow_notify_from',
+        'section' : 'incoming',
+        'type' : LType.ListSubnets,
+        'default' : '',
+        'help' : 'If set, NOTIFY requests from these comma separated netmasks will be allowed',
+        'doc' : '''
+Netmasks (both IPv4 and IPv6) that are allowed to issue NOTIFY operations
+to the server.  NOTIFY operations from IP addresses not listed here are
+ignored and do not get an answer.
+
+When the Proxy Protocol is enabled (see :ref:`setting-proxy-protocol-from`), the
+recursor will check the address of the client IP advertised in the
+Proxy Protocol header instead of the one of the proxy.
+
+Note that specifying an IP address without a netmask uses an implicit
+netmask of /32 or /128.
+
+NOTIFY operations received from a client listed in one of these netmasks
+will be accepted and used to wipe any cache entries whose zones match
+the zone specified in the NOTIFY operation, but only if that zone (or
+one of its parents) is included in :ref:`setting-allow-notify-for`,
+:ref:`setting-allow-notify-for-file`, or :ref:`setting-forward-zones-file` with a '^' prefix.
+ ''',
+        'doc-new' : '''
+Subnets (both IPv4 and IPv6) that are allowed to issue NOTIFY operations
+to the server.  NOTIFY operations from IP addresses not listed here are
+ignored and do not get an answer.
+
+When the Proxy Protocol is enabled (see :ref:`setting-proxy-protocol-from`), the
+recursor will check the address of the client IP advertised in the
+Proxy Protocol header instead of the one of the proxy.
+
+Note that specifying an IP address without a netmask uses an implicit
+netmask of /32 or /128.
+
+NOTIFY operations received from a client listed in one of these netmasks
+will be accepted and used to wipe any cache entries whose zones match
+the zone specified in the NOTIFY operation, but only if that zone (or
+one of its parents) is included in :ref:`setting-allow-notify-for`,
+:ref:`setting-allow-notify-for-file`, or :ref:`setting-forward-zones-file` with a ``allow_notify`` set to ``true``.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'allow_notify_from_file',
+        'section' : 'incoming',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, load NOTIFY-allowed netmasks from this file',
+        'doc' : '''
+Like :ref:`setting-allow-notify-from`, except reading from file. To use this
+feature, supply one netmask per line, with optional comments preceded
+by a '#'.
+ ''',
+        'doc-new' : '''
+Like :ref:`setting-allow-notify-from`, except reading a sequence of `Subnet`_ from file.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'any_to_tcp',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Answer ANY queries with tc=1, shunting to TCP',
+        'doc' : '''
+Answer questions for the ANY type on UDP with a truncated packet that refers the remote server to TCP.
+Useful for mitigating ANY reflection attacks.
+ ''',
+    }, 
+    {
+        'name' : 'allow_trust_anchor_query',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Allow queries for trustanchor.server CH TXT and negativetrustanchor.server CH TXT',
+        'doc' : '''
+Allow ``trustanchor.server CH TXT`` and ``negativetrustanchor.server CH TXT`` queries to view the configured :doc:`DNSSEC <dnssec>` (negative) trust anchors.
+ ''',
+    'versionadded': '4.3.0'
+    },
+    {
+        'name' : 'api_dir',
+        'section' : 'webservice',
+        'oldname' : 'api-config-dir',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Directory where REST API stores config and zones',
+        'doc' : '''
+Directory where the REST API stores its configuration and zones.
+For configuration updates to work, :ref:`setting-include-dir` should have the same value.
+ ''',
+    'versionadded': '4.0.0'
+     },
+    {
+        'name' : 'api_key',
+        'section' : 'webservice',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Static pre-shared authentication key for access to the REST API',
+        'doc' : '''
+Static pre-shared authentication key for access to the REST API. Since 4.6.0 the key can be hashed and salted using ``rec_control hash-password`` instead of being stored in the configuration in plaintext, but the plaintext version is still supported.
+ ''',
+        'versionadded': '4.0.0',
+        'versionchanged': ('4.6.0', 'This setting now accepts a hashed and salted version.')
+    },
+    {
+        'name' : 'auth_zones',
+        'section' : 'recursor',
+        'type' : LType.ListAuthZones,
+        'default' : '',
+        'help' : 'Zones for which we have authoritative data, comma separated domain=file pairs',
+        'doc' : '''
+Zones read from these files (in BIND format) are served authoritatively (but without the AA bit set in responses).
+DNSSEC is not supported. Example:
+
+.. code-block:: none
+
+    auth-zones=example.org=/var/zones/example.org, powerdns.com=/var/zones/powerdns.com
+ ''',
+        'doc-new' : '''
+Zones read from these files (in BIND format) are served authoritatively (but without the AA bit set in responses).
+DNSSEC is not supported. Example:
+
+.. code-block:: yaml
+
+ recursor:
+    auth-zones:
+    - zone: example.org
+      file: /var/zones/example.org
+    - zone: powerdns.com
+      file: /var/zones/powerdns.com
+ ''',
+    },
+    {
+        'name' : 'interval',
+        'section' : 'carbon',
+        'oldname' : 'carbon-interval',
+        'type' : LType.Uint64,
+        'default' : '30',
+        'help' : 'Number of seconds between carbon (graphite) updates',
+        'doc' : '''
+If sending carbon updates, this is the interval between them in seconds.
+See :doc:`metrics`.
+ ''',
+    },
+    {
+        'name' : 'ns',
+        'section' : 'carbon',
+        'oldname' : 'carbon-namespace',
+        'type' : LType.String,
+        'default' : 'pdns',
+        'help' : 'If set overwrites the first part of the carbon string',
+        'doc' : '''
+Change the namespace or first string of the metric key. The default is pdns.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'ourname',
+        'section' : 'carbon',
+        'oldname' : 'carbon-ourname',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, overrides our reported hostname for carbon stats',
+        'doc' : '''
+If sending carbon updates, if set, this will override our hostname.
+Be careful not to include any dots in this setting, unless you know what you are doing.
+See :ref:`metricscarbon`.
+ ''',
+    },
+    {
+        'name' : 'instance',
+        'section' : 'carbon',
+        'oldname' : 'carbon-instance',
+        'type' : LType.String,
+        'default' : 'recursor',
+        'help' : 'If set overwrites the instance name default',
+        'doc' : '''
+Change the instance or third string of the metric key. The default is recursor.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'server',
+        'section' : 'carbon',
+        'oldname' : 'carbon-server',
+        'type' : LType.ListSocketAddresses,
+        'default' : '',
+        'help' : 'If set, send metrics in carbon (graphite) format to this server IP address',
+        'doc' : '''
+If set to an IP or IPv6 address, will send all available metrics to this server via the carbon protocol, which is used by graphite and metronome. Moreover you can specify more than one server using a comma delimited list, ex: carbon-server=10.10.10.10,10.10.10.20.
+You may specify an alternate port by appending :port, for example: ``127.0.0.1:2004``.
+See :doc:`metrics`.
+ ''',
+        'doc-new' : '''
+Will send all available metrics to these servers via the carbon protocol, which is used by graphite and metronome.
+See :doc:`metrics`.
+ ''',
+    },
+    {
+        'name' : 'chroot',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'switch to chroot jail',
+        'doc' : '''
+If set, chroot to this directory for more security.
+This is not recommended; instead, we recommend containing PowerDNS using operating system features.
+We ship systemd unit files with our packages to make this easy.
+
+Make sure that ``/dev/log`` is available from within the chroot.
+Logging will silently fail over time otherwise (on logrotate).
+
+When using ``chroot``, all other paths (except for :ref:`setting-config-dir`) set in the configuration are relative to the new root.
+
+When running on a system where systemd manages services, ``chroot`` does not work out of the box, as PowerDNS cannot use the ``NOTIFY_SOCKET``.
+Either do not ``chroot`` on these systems or set the 'Type' of this service to 'simple' instead of 'notify' (refer to the systemd documentation on how to modify unit-files).
+ ''',
+    },
+    {
+        'name' : 'tcp_timeout',
+        'section' : 'incoming',
+        'oldname' : 'client-tcp-timeout',
+        'type' : LType.Uint64,
+        'default' : '2',
+        'help' : 'Timeout in seconds when talking to TCP clients',
+        'doc' : '''
+Time to wait for data from TCP clients.
+ ''',
+    },
+    {
+        'name' : 'config',
+        'section' : 'commands',
+        'type' : LType.Command,
+        'default' : 'no',
+        'help' : 'Output blank configuration. You can use --config=check to test the config file and command line arguments.',
+        'doc' : '''
+EMPTY?  '''
+    },
+    {
+        'name' : 'config_dir',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '/etc/powerdns',
+        'help' : 'Location of configuration directory (recursor.conf)',
+        'doc' : '''
+Location of configuration directory (``recursor.conf``).
+Usually ``/etc/powerdns``, but this depends on ``SYSCONFDIR`` during compile-time.
+ ''',
+    },
+    {
+        'name' : 'config_name',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Name of this virtual configuration - will rename the binary image',
+        'doc' : '''
+When running multiple recursors on the same server, read settings from :file:`recursor-{name}.conf`, this will also rename the binary image.
+ ''',
+    },
+    {
+        'name' : 'cpu_map',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Thread to CPU mapping, space separated thread-id=cpu1,cpu2..cpuN pairs',
+        'doc' : '''
+Set CPU affinity for threads, asking the scheduler to run those threads on a single CPU, or a set of CPUs.
+This parameter accepts a space separated list of thread-id=cpu-id, or thread-id=cpu-id-1,cpu-id-2,...,cpu-id-N.
+For example, to make the worker thread 0 run on CPU id 0 and the worker thread 1 on CPUs 1 and 2::
+
+  cpu-map=0=0 1=1,2
+
+The thread handling the control channel, the webserver and other internal stuff has been assigned id 0, the distributor
+threads if any are assigned id 1 and counting, and the worker threads follow behind.
+The number of distributor threads is determined by :ref:`setting-distributor-threads`, the number of worker threads is determined by the :ref:`setting-threads` setting.
+
+This parameter is only available if the OS provides the ``pthread_setaffinity_np()`` function.
+
+Note that depending on the configuration the Recursor can start more threads.
+Typically these threads will sleep most of the time.
+These threads cannot be specified in this setting as their thread-ids are left unspecified.
+ ''',
+        'doc' : '''
+Set CPU affinity for threads, asking the scheduler to run those threads on a single CPU, or a set of CPUs.
+This parameter accepts a space separated list of thread-id=cpu-id, or thread-id=cpu-id-1,cpu-id-2,...,cpu-id-N.
+For example, to make the worker thread 0 run on CPU id 0 and the worker thread 1 on CPUs 1 and 2::
+
+.. code-block:: yaml
+
+  recursor:
+    cpu_map: 0=0 1=1,2
+
+The thread handling the control channel, the webserver and other internal stuff has been assigned id 0, the distributor
+threads if any are assigned id 1 and counting, and the worker threads follow behind.
+The number of distributor threads is determined by :ref:`setting-distributor-threads`, the number of worker threads is determined by the :ref:`setting-threads` setting.
+
+This parameter is only available if the OS provides the ``pthread_setaffinity_np()`` function.
+
+Note that depending on the configuration the Recursor can start more threads.
+Typically these threads will sleep most of the time.
+These threads cannot be specified in this setting as their thread-ids are left unspecified.
+ ''',
+    },
+    {
+        'name' : 'daemon',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Operate as a daemon',
+        'doc' : '''
+Operate in the background.
+ ''',
+        'versionchanged': ('4.0.0', 'Default is now ``no``, was ``yes`` before.')
+    },
+    {
+        'name' : 'dont_throttle_names',
+        'section' : 'outgoing',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'Do not throttle nameservers with this name or suffix',
+        'doc' : '''
+When an authoritative server does not answer a query or sends a reply the recursor does not like, it is throttled.
+Any servers' name suffix-matching the supplied names will never be throttled.
+
+.. warning::
+  Most servers on the internet do not respond for a good reason (overloaded or unreachable), ``dont-throttle-names`` could make this load on the upstream server even higher, resulting in further service degradation.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'dont_throttle_netmasks',
+        'section' : 'outgoing',
+        'type' : LType.ListSubnets,
+        'default' : '',
+        'help' : 'Do not throttle nameservers with this IP netmask',
+        'doc' : '''
+When an authoritative server does not answer a query or sends a reply the recursor does not like, it is throttled.
+Any servers matching the supplied netmasks will never be throttled.
+
+This can come in handy on lossy networks when forwarding, where the same server is configured multiple times (e.g. with ``forward-zones-recurse=example.com=192.0.2.1;192.0.2.1``).
+By default, the PowerDNS Recursor would throttle the 'first' server on a timeout and hence not retry the 'second' one.
+In this case, ``dont-throttle-netmasks`` could be set to ``192.0.2.1``.
+
+.. warning::
+  Most servers on the internet do not respond for a good reason (overloaded or unreachable), ``dont-throttle-netmasks`` could make this load on the upstream server even higher, resulting in further service degradation.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'devonly_regression_test_mode',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'internal use only',
+        'doc' : 'SKIP',
+    },
+    {
+        'name' : 'disable',
+        'section' : 'packetcache',
+        'oldname' : 'disable-packetcache',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Disable packetcache',
+        'doc' : '''
+Turn off the packet cache. Useful when running with Lua scripts that can not be cached, though individual query caching can be controlled from Lua as well.
+ ''',
+    },
+    {
+        'name' : 'disable_syslog',
+        'section' : 'logging',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Disable logging to syslog, useful when running inside a supervisor that logs stdout',
+        'doc' : '''
+Do not log to syslog, only to stdout.
+Use this setting when running inside a supervisor that handles logging (like systemd).
+**Note**: do not use this setting in combination with :ref:`setting-daemon` as all logging will disappear.
+ ''',
+    },
+    {
+        'name' : 'distribution_load_factor',
+        'section' : 'incoming',
+        'type' : LType.Double,
+        'default' : '0.0',
+        'help' : 'The load factor used when PowerDNS is distributing queries to worker threads',
+        'doc' : '''
+If :ref:`setting-pdns-distributes-queries` is set and this setting is set to another value
+than 0, the distributor thread will use a bounded load-balancing algorithm while
+distributing queries to worker threads, making sure that no thread is assigned
+more queries than distribution-load-factor times the average number of queries
+currently processed by all the workers.
+For example, with a value of 1.25, no server should get more than 125 % of the
+average load. This helps making sure that all the workers have roughly the same
+share of queries, even if the incoming traffic is very skewed, with a larger
+number of requests asking for the same qname.
+ ''',
+    'versionadded': '4.1.12'
+    },
+    {
+        'name' : 'distribution_pipe_buffer_size',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Size in bytes of the internal buffer of the pipe used by the distributor to pass incoming queries to a worker thread',
+        'doc' : '''
+Size in bytes of the internal buffer of the pipe used by the distributor to pass incoming queries to a worker thread.
+Requires support for `F_SETPIPE_SZ` which is present in Linux since 2.6.35. The actual size might be rounded up to
+a multiple of a page size. 0 means that the OS default size is used.
+A large buffer might allow the recursor to deal with very short-lived load spikes during which a worker thread gets
+overloaded, but it will be at the cost of an increased latency.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'distributor_threads',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'docdefault' : '1 if :ref:`setting-pdns-distributes-queries` is set, 0 otherwise',
+        'help' : 'Launch this number of distributor threads, distributing queries to other threads',
+        'doc' : '''
+If :ref:`setting-pdns-distributes-queries` is set, spawn this number of distributor threads on startup. Distributor threads
+handle incoming queries and distribute them to other threads based on a hash of the query.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'dot_to_auth_names',
+        'section' : 'outgoing',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'Use DoT to authoritative servers with these names or suffixes',
+        'doc' : '''
+Force DoT to the listed authoritative nameservers. For this to work, DoT support has to be compiled in.
+Currently, the certificate is not checked for validity in any way.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'dot_to_port_853',
+        'section' : 'outgoing',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Force DoT connection to target port 853 if DoT compiled in',
+        'doc' : '''
+Enable DoT to forwarders that specify port 853.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'dns64_prefix',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'DNS64 prefix',
+        'doc' : '''
+Enable DNS64 (:rfc:`6147`) support using the supplied /96 IPv6 prefix. This will generate 'fake' ``AAAA`` records for names
+with only ``A`` records, as well as 'fake' ``PTR`` records to make sure that reverse lookup of DNS64-generated IPv6 addresses
+generate the right name.
+See :doc:`dns64` for more flexible but slower alternatives using Lua.
+ ''',
+    'versionadded': '4.4.0'
+    },
+    {
+        'name' : 'validation',
+        'section' : 'dnssec',
+        'oldname' : 'dnssec',
+        'type' : LType.String,
+        'default' : 'process',
+        'help' : 'DNSSEC mode: off/process-no-validate/process (default)/log-fail/validate',
+        'doc' : '''
+One of ``off``, ``process-no-validate``, ``process``, ``log-fail``, ``validate``
+
+Set the mode for DNSSEC processing, as detailed in :doc:`dnssec`.
+
+``off``
+   No DNSSEC processing whatsoever.
+   Ignore DO-bits in queries, don't request any DNSSEC information from authoritative servers.
+   This behaviour is similar to PowerDNS Recursor pre-4.0.
+``process-no-validate``
+   Respond with DNSSEC records to clients that ask for it, set the DO bit on all outgoing queries.
+   Don't do any validation.
+``process``
+   Respond with DNSSEC records to clients that ask for it, set the DO bit on all outgoing queries.
+   Do validation for clients that request it (by means of the AD- bit or DO-bit in the query).
+``log-fail``
+   Similar behaviour to ``process``, but validate RRSIGs on responses and log bogus responses.
+``validate``
+   Full blown DNSSEC validation. Send SERVFAIL to clients on bogus responses.
+ ''',
+        'versionadded': '4.0.0',
+        'versionchanged': ('4.5.0',
+   'The default changed from ``process-no-validate`` to ``process``')
+    },
+    {
+        'name' : 'disabled_algorithms',
+        'section' : 'dnssec',
+        'oldname' : 'dnssec-disabled-algorithms',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'List of DNSSEC algorithm numbers that are considered unsupported',
+        'doc' : '''
+A list of DNSSEC algorithm numbers that should be considered disabled.
+These algorithms will not be used to validate DNSSEC signatures.
+Zones (only) signed with these algorithms will be considered ``Insecure``.
+
+If this setting is empty (the default), :program:`Recursor` will determine which algorithms to disable automatically.
+This is done for specific algorithms only, currently algorithms 5 (``RSASHA1``) and 7 (``RSASHA1NSEC3SHA1``).
+
+This is important on systems that have a default strict crypto policy, like RHEL9 derived systems.
+On such systems not disabling some algorithms (or changing the security policy) will make affected zones to be considered ``Bogus`` as using these algorithms fails.
+ ''',
+    'versionadded': '4.9.0'
+    },
+    {
+        'name' : 'log_bogus',
+        'section' : 'dnssec',
+        'oldname' : 'dnssec-log-bogus',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Log DNSSEC bogus validations',
+        'doc' : '''
+Log every DNSSEC validation failure.
+**Note**: This is not logged per-query but every time records are validated as Bogus.
+ ''',
+    },
+    {
+        'name' : 'dont_query',
+        'section' : 'outgoing',
+        'type' : LType.ListSubnets,
+        'default' : '127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10, 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 240.0.0.0/4, ::/96, ::ffff:0:0/96, 100::/64, 2001:db8::/32',
+        'help' : 'If set, do not query these netmasks for DNS data',
+        'doc' : '''
+The DNS is a public database, but sometimes contains delegations to private IP addresses, like for example 127.0.0.1.
+This can have odd effects, depending on your network, and may even be a security risk.
+Therefore, the PowerDNS Recursor by default does not query private space IP addresses.
+This setting can be used to expand or reduce the limitations.
+
+Queries for names in forward zones and to addresses as configured in any of the settings :ref:`setting-forward-zones`, :ref:`setting-forward-zones-file` or :ref:`setting-forward-zones-recurse` are performed regardless of these limitations.
+ ''',
+    },
+    {
+        'name' : 'add_for',
+        'section' : 'ecs',
+        'oldname' : 'ecs-add-for',
+        'type' : LType.ListSubnets,
+        'default' : '0.0.0.0/0, ::/0, !127.0.0.0/8, !10.0.0.0/8, !100.64.0.0/10, !169.254.0.0/16, !192.168.0.0/16, !172.16.0.0/12, !::1/128, !fc00::/7, !fe80::/10',
+        'help' : 'List of client netmasks for which EDNS Client Subnet will be added',
+        'doc' : '''
+List of requestor netmasks for which the requestor IP Address should be used as the :rfc:`EDNS Client Subnet <7871>` for outgoing queries. Outgoing queries for requestors that do not match this list will use the :ref:`setting-ecs-scope-zero-address` instead.
+Valid incoming ECS values from :ref:`setting-use-incoming-edns-subnet` are not replaced.
+
+Regardless of the value of this setting, ECS values are only sent for outgoing queries matching the conditions in the :ref:`setting-edns-subnet-allow-list` setting. This setting only controls the actual value being sent.
+
+This defaults to not using the requestor address inside RFC1918 and similar 'private' IP address spaces.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'ipv4_bits',
+        'section' : 'ecs',
+        'oldname' : 'ecs-ipv4-bits',
+        'type' : LType.Uint64,
+        'default' : '24',
+        'help' : 'Number of bits of IPv4 address to pass for EDNS Client Subnet',
+        'doc' : '''
+Number of bits of client IPv4 address to pass when sending EDNS Client Subnet address information.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'ipv4_cache_bits',
+        'section' : 'ecs',
+        'oldname' : 'ecs-ipv4-cache-bits',
+        'type' : LType.Uint64,
+        'default' : '24',
+        'help' : 'Maximum number of bits of IPv4 mask to cache ECS response',
+        'doc' : '''
+Maximum number of bits of client IPv4 address used by the authoritative server (as indicated by the EDNS Client Subnet scope in the answer) for an answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-cache-limit-ttl``.
+That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
+ ''',
+    'versionadded': '4.1.12'
+    },
+    {
+        'name' : 'ipv6_bits',
+        'section' : 'ecs',
+        'oldname' : 'ecs-ipv6-bits',
+        'type' : LType.Uint64,
+        'default' : '56',
+        'help' : 'Number of bits of IPv6 address to pass for EDNS Client Subnet',
+        'doc' : '''
+Number of bits of client IPv6 address to pass when sending EDNS Client Subnet address information.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'ipv6_cache_bits',
+        'section' : 'ecs',
+        'oldname' : 'ecs-ipv6-cache-bits',
+        'type' : LType.Uint64,
+        'default' : '56',
+        'help' : 'Maximum number of bits of IPv6 mask to cache ECS response',
+        'doc' : '''
+Maximum number of bits of client IPv6 address used by the authoritative server (as indicated by the EDNS Client Subnet scope in the answer) for an answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-cache-limit-ttl``.
+That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
+ ''',
+    'versionadded': '4.1.12'
+    },
+    {
+        'name' : 'ipv4_never_cache',
+        'section' : 'ecs',
+        'oldname' : 'ecs-ipv4-never-cache',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If we should never cache IPv4 ECS responses',
+        'doc' : '''
+When set, never cache replies carrying EDNS IPv4 Client Subnet scope in the record cache.
+In this case the decision made by ```ecs-ipv4-cache-bits`` and ``ecs-cache-limit-ttl`` is no longer relevant.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'ipv6_never_cache',
+        'section' : 'ecs',
+        'oldname' : 'ecs-ipv6-never-cache',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If we should never cache IPv6 ECS responses',
+        'doc' : '''
+When set, never cache replies carrying EDNS IPv6 Client Subnet scope in the record cache.
+In this case the decision made by ```ecs-ipv6-cache-bits`` and ``ecs-cache-limit-ttl`` is no longer relevant.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'minimum_ttl_override',
+        'section' : 'ecs',
+        'oldname' : 'ecs-minimum-ttl-override',
+        'type' : LType.Uint64,
+        'default' : '1',
+        'help' : 'The minimum TTL for records in ECS-specific answers',
+        'doc' : '''
+This setting artificially raises the TTLs of records in the ANSWER section of ECS-specific answers to be at least this long.
+Setting this to a value greater than 1 technically is an RFC violation, but might improve performance a lot.
+Using a value of 0 impacts performance of TTL 0 records greatly, since it forces the recursor to contact
+authoritative servers every time a client requests them.
+Can be set at runtime using ``rec_control set-ecs-minimum-ttl 3600``.
+ ''',
+        'versionchanged': ('4.5.0', 'Old versions used default 0.')
+    },
+    {
+        'name' : 'cache_limit_ttl',
+        'section' : 'ecs',
+        'oldname' : 'ecs-cache-limit-ttl',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Minimum TTL to cache ECS response',
+        'doc' : '''
+The minimum TTL for an ECS-specific answer to be inserted into the query cache. This condition applies in conjunction with ``ecs-ipv4-cache-bits`` or ``ecs-ipv6-cache-bits``.
+That is, only if both the limits apply, the record will not be cached. This decision can be overridden by ``ecs-ipv4-never-cache`` and ``ecs-ipv6-never-cache``.
+ ''',
+    'versionadded': '4.1.12'
+    },
+    {
+        'name' : 'scope_zero_address',
+        'section' : 'ecs',
+        'oldname' : 'ecs-scope-zero-address',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Address to send to allow-listed authoritative servers for incoming queries with ECS prefix-length source of 0',
+        'doc' : '''
+The IP address sent via EDNS Client Subnet to authoritative servers listed in
+:ref:`setting-edns-subnet-allow-list` when :ref:`setting-use-incoming-edns-subnet` is set and the query has
+an ECS source prefix-length set to 0.
+The default is to look for the first usable (not an ``any`` one) address in
+:ref:`setting-query-local-address` (starting with IPv4). If no suitable address is
+found, the recursor fallbacks to sending 127.0.0.1.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'edns_bufsize',
+        'section' : 'outgoing',
+        'oldname' : 'edns-outgoing-bufsize',
+        'type' : LType.Uint64,
+        'default' : '1232',
+        'help' : 'Outgoing EDNS buffer size',
+        'doc' : '''
+.. note:: Why 1232?
+
+  1232 is the largest number of payload bytes that can fit in the smallest IPv6 packet.
+  IPv6 has a minimum MTU of 1280 bytes (:rfc:`RFC 8200, section 5 <8200#section-5>`), minus 40 bytes for the IPv6 header, minus 8 bytes for the UDP header gives 1232, the maximum payload size for the DNS response.
+
+This is the value set for the EDNS0 buffer size in outgoing packets.
+Lower this if you experience timeouts.
+ ''',
+     'versionchanged': ('4.2.0', 'Before 4.2.0, the default was 1680')
+    },
+    {
+        'name' : 'edns_padding_from',
+        'section' : 'incoming',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'List of netmasks (proxy IP in case of proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses, provided that \'edns-padding-mode\' applies',
+        'doc' : '''
+List of netmasks (proxy IP in case of proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses, provided that :ref:`setting-edns-padding-mode` applies.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'edns_padding_mode',
+        'section' : 'incoming',
+        'type' : LType.String,
+        'default' : 'padded-queries-only',
+        'help' : 'Whether to add EDNS padding to all responses (\'always\') or only to responses for queries containing the EDNS padding option (\'padded-queries-only\', the default). In both modes, padding will only be added to responses for queries coming from \'setting-edns-padding-from\' sources',
+        'doc' : '''
+One of ``always``, ``padded-queries-only``.
+Whether to add EDNS padding to all responses (``always``) or only to responses for queries containing the EDNS padding option (``padded-queries-only``, the default).
+In both modes, padding will only be added to responses for queries coming from :ref:`setting-edns-padding-from` sources.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'edns_padding',
+        'section' : 'outgoing',
+        'oldname' : 'edns-padding-out',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Whether to add EDNS padding to outgoing DoT messages',
+        'doc' : '''
+Whether to add EDNS padding to outgoing DoT queries.
+ ''',
+    'versionadded': '4.8.0'
+    },
+    {
+        'name' : 'edns_padding_tag',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '7830',
+        'help' : 'Packetcache tag associated to responses sent with EDNS padding, to prevent sending these to clients for which padding is not enabled.',
+        'doc' : '''
+The packetcache tag to use for padded responses, to prevent a client not allowed by the :ref::`setting-edns-padding-from` list to be served a cached answer generated for an allowed one. This
+effectively divides the packet cache in two when :ref:`setting-edns-padding-from` is used. Note that this will not override a tag set from one of the ``Lua`` hooks.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'edns_subnet_whitelist',
+        'section' : 'outgoing',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'List of netmasks and domains that we should enable EDNS subnet for (deprecated)',
+        'doc' : '',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-edns-subnet-allow-list`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'edns_subnet_allow_list',
+        'section' : 'outgoing',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'List of netmasks and domains that we should enable EDNS subnet for',
+        'doc' : '''
+List of netmasks and domains that :rfc:`EDNS Client Subnet <7871>` should be enabled for in outgoing queries.
+
+For example, an EDNS Client Subnet option containing the address of the initial requestor (but see :ref:`setting-ecs-add-for`) will be added to an outgoing query sent to server 192.0.2.1 for domain X if 192.0.2.1 matches one of the supplied netmasks, or if X matches one of the supplied domains.
+The initial requestor address will be truncated to 24 bits for IPv4 (see :ref:`setting-ecs-ipv4-bits`) and to 56 bits for IPv6 (see :ref:`setting-ecs-ipv6-bits`), as recommended in the privacy section of RFC 7871.
+
+
+Note that this setting describes the destination of outgoing queries, not the sources of incoming queries, nor the subnets described in the EDNS Client Subnet option.
+
+By default, this option is empty, meaning no EDNS Client Subnet information is sent.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'entropy_source',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '/dev/urandom',
+        'help' : 'If set, read entropy from this file',
+        'doc' : '''
+PowerDNS can read entropy from a (hardware) source.
+This is used for generating random numbers which are very hard to predict.
+Generally on UNIX platforms, this source will be ``/dev/urandom``, which will always supply random numbers, even if entropy is lacking.
+Change to ``/dev/random`` if PowerDNS should block waiting for enough entropy to arrive.
+ ''',
+    },
+    {
+        'name' : 'etc_hosts_file',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '/etc/hosts',
+        'help' : 'Path to \'hosts\' file',
+        'doc' : '''
+The path to the /etc/hosts file, or equivalent.
+This file can be used to serve data authoritatively using :ref:`setting-export-etc-hosts`.
+ ''',
+    },
+    {
+        'name' : 'event_trace_enabled',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'If set, event traces are collected and send out via protobuf logging (1), logfile (2) or both(3)',
+        'doc' : '''
+Enable the recording and logging of ref:`event traces`. This is an experimental feature and subject to change.
+Possible values are 0: (disabled), 1 (add information to protobuf logging messages) and 2 (write to log) and 3 (both).
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'export_etc_hosts',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If we should serve up contents from /etc/hosts',
+        'doc' : '''
+If set, this flag will export the host names and IP addresses mentioned in ``/etc/hosts``.
+ ''',
+    },
+    {
+        'name' : 'export_etc_hosts_search_suffix',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Also serve up the contents of /etc/hosts with this suffix',
+        'doc' : '''
+If set, all hostnames in the :ref:`setting-export-etc-hosts` file are loaded in canonical form, based on this suffix, unless the name contains a '.', in which case the name is unchanged.
+So an entry called 'pc' with ``export-etc-hosts-search-suffix='home.com'`` will lead to the generation of 'pc.home.com' within the recursor.
+An entry called 'server1.home' will be stored as 'server1.home', regardless of this setting.
+ ''',
+    },
+    {
+        'name' : 'extended_resolution_errors',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If set, send an EDNS Extended Error extension on resolution failures, like DNSSEC validation errors',
+        'doc' : '''
+If set, the recursor will add an EDNS Extended Error (:rfc:`8914`) to responses when resolution failed, like DNSSEC validation errors, explaining the reason it failed. This setting is not needed to allow setting custom error codes from Lua or from a RPZ hit.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'forward_zones',
+        'section' : 'recursor',
+        'type' : LType.ListForwardZones,
+        'default' : '',
+        'help' : 'Zones for which we forward queries, comma separated domain=ip pairs',
+        'doc' : '''
+Queries for zones listed here will be forwarded to the IP address listed. i.e.
+
+.. code-block:: none
+
+    forward-zones=example.org=203.0.113.210, powerdns.com=2001:DB8::BEEF:5
+
+Multiple IP addresses can be specified and port numbers other than 53 can be configured:
+
+.. code-block:: none
+
+    forward-zones=example.org=203.0.113.210:5300;127.0.0.1, powerdns.com=127.0.0.1;198.51.100.10:530;[2001:DB8::1:3]:5300
+
+Forwarded queries have the ``recursion desired (RD)`` bit set to ``0``, meaning that this setting is intended to forward queries to authoritative servers.
+If an ``NS`` record set for a subzone of the forwarded zone is learned, that record set will be used to determine addresses for name servers of the subzone.
+This allows e.g. a forward to a local authoritative server holding a copy of the root zone, delegations received from that server will work.
+
+**IMPORTANT**: When using DNSSEC validation (which is default), forwards to non-delegated (e.g. internal) zones that have a DNSSEC signed parent zone will validate as Bogus.
+To prevent this, add a Negative Trust Anchor (NTA) for this zone in the :ref:`setting-lua-config-file` with ``addNTA('your.zone', 'A comment')``.
+If this forwarded zone is signed, instead of adding NTA, add the DS record to the :ref:`setting-lua-config-file`.
+See the :doc:`dnssec` information.
+ ''',
+        'doc-new' : '''
+Queries for zones listed here will be forwarded to the IP address listed. i.e.
+
+.. code-block:: yaml
+
+ recursor:
+    forward-zones:
+      - zone: example.org
+        forwarders:
+        - 203.0.113.210
+      - zone: powerdns.com
+        forwarders:
+        - 2001:DB8::BEEF:5
+
+Multiple IP addresses can be specified and port numbers other than 53 can be configured:
+
+.. code-block:: yaml
+
+  recursor:
+    forward-zones:
+    - zone: example.org
+      forwarders:
+      - 203.0.113.210:5300
+      - 127.0.0.1
+    - zone: powerdns.com
+      forwarders:
+      - 127.0.0.1
+      - 198.51.100.10:530
+      - '[2001:DB8::1:3]:5300'
+
+Forwarded queries have the ``recursion desired (RD)`` bit set to ``0``, meaning that this setting is intended to forward queries to authoritative servers.
+If an ``NS`` record set for a subzone of the forwarded zone is learned, that record set will be used to determine addresses for name servers of the subzone.
+This allows e.g. a forward to a local authoritative server holding a copy of the root zone, delegations received from that server will work.
+
+**IMPORTANT**: When using DNSSEC validation (which is default), forwards to non-delegated (e.g. internal) zones that have a DNSSEC signed parent zone will validate as Bogus.
+To prevent this, add a Negative Trust Anchor (NTA) for this zone in the :ref:`setting-lua-config-file` with ``addNTA('your.zone', 'A comment')``.
+If this forwarded zone is signed, instead of adding NTA, add the DS record to the :ref:`setting-lua-config-file`.
+See the :doc:`dnssec` information.
+ ''',
+    },
+    {
+        'name' : 'forward_zones_file',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'File with (+)domain=ip pairs for forwarding',
+        'doc' : '''
+Same as :ref:`setting-forward-zones`, parsed from a file. Only 1 zone is allowed per line, specified as follows:
+
+.. code-block:: none
+
+    example.org=203.0.113.210, 192.0.2.4:5300
+
+Zones prefixed with a ``+`` are treated as with
+:ref:`setting-forward-zones-recurse`.  Default behaviour without ``+`` is as with
+:ref:`setting-forward-zones`.
+
+The DNSSEC notes from :ref:`setting-forward-zones` apply here as well.
+ ''',
+    'doc-new' : '''
+        Same as :ref:`setting-forward-zones`, parsed from a file as a sequence of `ZoneForward`.
+
+.. code-block:: yaml
+
+  - zone: example1.com
+    forwarders:
+    - 127.0.0.1
+    - 127.0.0.1:5353
+    - '[::1]53'
+  - zone: example2.com
+    forwarders:
+    - ::1
+    recurse: true
+    notify_allowed: true
+
+The DNSSEC notes from :ref:`setting-forward-zones` apply here as well.
+ ''',
+     'versionchanged': [('4.0.0', '(Old style settings only) Comments are allowed, everything behind ``#`` is ignored.'),
+                        ('4.6.0', '(Old style settings only) Zones prefixed with a ``^`` are added to the :ref:`setting-allow-notify-for` list. Both prefix characters can be used if desired, in any order.')],
+    },
+    {
+        'name' : 'forward_zones_recurse',
+        'section' : 'recursor',
+        'type' : LType.ListForwardZones,
+        'default' : '',
+        'help' : 'Zones for which we forward queries with recursion bit, comma separated domain=ip pairs',
+        'doc' : '''
+Like regular :ref:`setting-forward-zones`, but forwarded queries have the ``recursion desired (RD)`` bit set to ``1``, meaning that this setting is intended to forward queries to other recursive servers.
+In contrast to regular forwarding, the rule that delegations of the forwarded subzones are respected is not active.
+This is because we rely on the forwarder to resolve the query fully.
+
+See :ref:`setting-forward-zones` for additional options (such as supplying multiple recursive servers) and an important note about DNSSEC.
+ ''',
+    },
+    {
+        'name' : 'gettag_needs_edns_options',
+        'section' : 'incoming',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If EDNS Options should be extracted before calling the gettag() hook',
+        'doc' : '''
+If set, EDNS options in incoming queries are extracted and passed to the :func:`gettag` hook in the ``ednsoptions`` table.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'help',
+        'section' : 'commands',
+        'type' : LType.Command,
+        'default' : 'no',
+        'help' : 'Provide a helpful message',
+        'doc' : '''
+EMPTY?  '''
+    },
+    {
+        'name' : 'hint_file',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, load root hints from this file',
+        'doc' : '''
+If set, the root-hints are read from this file. If empty, the default built-in root hints are used.
+
+In some special cases, processing the root hints is not needed, for example when forwarding all queries to another recursor.
+For these special cases, it is possible to disable the processing of root hints by setting the value to ``no`` or ``no-refresh``.
+See :ref:`handling-of-root-hints` for more information on root hints handling.
+ ''',
+        'versionchanged': [('4.6.2', 'Introduced the value ``no`` to disable root-hints processing.'),
+                           ('4.9.0', 'Introduced the value ``no-refresh`` to disable both root-hints processing and periodic refresh of the cached root `NS` records.')]
+    },
+    {
+        'name' : 'ignore_unknown_settings',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'Configuration settings to ignore if they are unknown',
+        'doc' : '''
+Names of settings to be ignored while parsing configuration files, if the setting
+name is unknown to PowerDNS.
+
+Useful during upgrade testing.
+ ''',
+    },
+    {
+        'name' : 'include_dir',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Include *.conf files from this directory',
+        'doc' : '''
+Directory to scan for additional config files. All files that end with .conf are loaded in order using ``POSIX`` as locale.
+ ''',
+    },
+    {
+        'name' : 'latency_statistic_size',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '10000',
+        'help' : 'Number of latency values to calculate the qa-latency average',
+        'doc' : '''
+Indication of how many queries will be averaged to get the average latency reported by the 'qa-latency' metric.
+ ''',
+    },
+    {
+        'name' : 'listen',
+        'section' : 'incoming',
+        'oldname' : 'local-address',
+        'type' : LType.ListSocketAddresses,
+        'default' : '127.0.0.1',
+        'help' : 'IP addresses to listen on, separated by spaces or commas. Also accepts ports.',
+        'doc' : '''
+Local IP addresses to which we bind. Each address specified can
+include a port number; if no port is included then the
+:ref:`setting-local-port` port will be used for that address. If a
+port number is specified, it must be separated from the address with a
+':'; for an IPv6 address the address must be enclosed in square
+brackets.
+
+Examples::
+
+  local-address=127.0.0.1 ::1
+  local-address=0.0.0.0:5353
+  local-address=[::]:8053
+  local-address=127.0.0.1:53, [::1]:5353
+ ''',
+    },
+    {
+        'name' : 'port',
+        'section' : 'incoming',
+        'oldname' : 'local-port',
+        'type' : LType.Uint64,
+        'default' : '53',
+        'help' : 'port to listen on',
+        'doc' : '''
+Local port to bind to.
+If an address in :ref:`setting-local-address` does not have an explicit port, this port is used.
+ ''',
+    },
+    {
+        'name' : 'timestamp',
+        'section' : 'logging',
+        'oldname' : 'log-timestamp',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Print timestamps in log lines, useful to disable when running with a tool that timestamps stdout already',
+        'doc' : '''
+
+ ''',
+    },
+    {
+        'name' : 'non_local_bind',
+        'section' : 'incoming',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Enable binding to non-local addresses by using FREEBIND / BINDANY socket options',
+        'doc' : '''
+Bind to addresses even if one or more of the :ref:`setting-local-address`'s do not exist on this server.
+Setting this option will enable the needed socket options to allow binding to non-local addresses.
+This feature is intended to facilitate ip-failover setups, but it may also mask configuration issues and for this reason it is disabled by default.
+ ''',
+    },
+    {
+        'name' : 'loglevel',
+        'section' : 'logging',
+        'type' : LType.Uint64,
+        'default' : '6',
+        'help' : 'Amount of logging. Higher is more. Do not set below 3',
+        'doc' : '''
+Amount of logging. The higher the number, the more lines logged.
+Corresponds to 'syslog' level values (e.g. 0 = emergency, 1 = alert, 2 = critical, 3 = error, 4 = warning, 5 = notice, 6 = info, 7 = debug).
+Each level includes itself plus the lower levels before it.
+Not recommended to set this below 3.
+ ''',
+    },    
+    {
+        'name' : 'common_errors',
+        'section' : 'logging',
+        'oldname' : 'log-common-errors',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If we should log rather common errors',
+        'doc' : '''
+Some DNS errors occur rather frequently and are no cause for alarm.
+ ''',
+    },
+    {
+        'name' : 'rpz_changes',
+        'section' : 'logging',
+        'oldname' : 'log-rpz-changes',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Log additions and removals to RPZ zones at Info level',
+        'doc' : '''
+Log additions and removals to RPZ zones at Info (6) level instead of Debug (7).
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'facility',
+        'section' : 'logging',
+        'oldname' : 'logging-facility',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Facility to log messages as. 0 corresponds to local0',
+        'doc' : '''
+If set to a digit, logging is performed under this LOCAL facility.
+See :ref:`logging`.
+Do not pass names like 'local0'!
+ ''',
+    },
+    {
+        'name' : 'lowercase',
+        'section' : 'outgoing',
+        'oldname' : 'lowercase-outgoing',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Force outgoing questions to lowercase',
+        'doc' : '''
+Set to true to lowercase the outgoing queries.
+When set to 'no' (the default) a query from a client using mixed case in the DNS labels (such as a user entering mixed-case names or `draft-vixie-dnsext-dns0x20-00 <http://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00>`_), PowerDNS preserves the case of the query.
+Broken authoritative servers might give a wrong or broken answer on this encoding.
+Setting ``lowercase-outgoing`` to 'yes' makes the PowerDNS Recursor lowercase all the labels in the query to the authoritative servers, but still return the proper case to the client requesting.
+ ''',
+    },
+    {
+        'name' : 'lua_config_file',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'More powerful configuration options',
+        'doc' : '''
+If set, and Lua support is compiled in, this will load an additional configuration file for newer features and more complicated setups.
+See :doc:`lua-config/index` for the options that can be set in this file.
+ ''',
+    },
+    {
+        'name' : 'lua_dns_script',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Filename containing an optional Lua script that will be used to modify dns answers',
+        'doc' : '''
+Path to a lua file to manipulate the Recursor's answers. See :doc:`lua-scripting/index` for more information.
+ ''',
+    },
+    {
+        'name' : 'lua_maintenance_interval',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '1',
+        'help' : 'Number of seconds between calls to the lua user defined maintenance() function',
+        'doc' : '''
+The interval between calls to the Lua user defined `maintenance()` function in seconds.
+See :ref:`hooks-maintenance-callback`
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'max_busy_dot_probes',
+        'section' : 'outgoing',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Maximum number of concurrent DoT probes',
+        'doc' : '''
+Limit the maximum number of simultaneous DoT probes the Recursor will schedule.
+The default value 0 means no DoT probes are scheduled.
+
+DoT probes are used to check if an authoritative server's IP address supports DoT.
+If the probe determines an IP address supports DoT, the Recursor will use DoT to contact it for subsequent queries until a failure occurs.
+After a failure, the Recursor will stop using DoT for that specific IP address for a while.
+The results of probes are remembered and can be viewed by the ``rec_control dump-dot-probe-map`` command.
+If the maximum number of pending probes is reached, no probes will be scheduled, even if no DoT status is known for an address.
+If the result of a probe is not yet available, the Recursor will contact the authoritative server in the regular way, unless an authoritative server is configured to be contacted over DoT always using :ref:`setting-dot-to-auth-names`.
+In that case no probe will be scheduled.
+
+.. note::
+  DoT probing is an experimental feature.
+  Please test thoroughly to determine if it is suitable in your specific production environment before enabling. 
+ ''',
+    'versionadded': '4.7.0'
+    },
+    {
+        'name' : 'max_cache_bogus_ttl',
+        'section' : 'recordcache',
+        'type' : LType.Uint64,
+        'default' : '3600',
+        'help' : 'maximum number of seconds to keep a Bogus (positive or negative) cached entry in memory',
+        'doc' : '''
+Maximum number of seconds to cache an item in the DNS cache (negative or positive) if its DNSSEC validation failed, no matter what the original TTL specified, to reduce the impact of a broken domain.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'max_entries',
+        'section' : 'recordcache',
+        'oldname' : 'max-cache-entries',
+        'type' : LType.Uint64,
+        'default' : '1000000',
+        'help' : 'If set, maximum number of entries in the main cache',
+        'doc' : '''
+Maximum number of DNS record cache entries, shared by all threads since 4.4.0.
+Each entry associates a name and type with a record set.
+The size of the negative cache is 10% of this number.
+ ''',
+    },
+    {
+        'name' : 'max_ttl',
+        'section' : 'recordcache',
+        'oldname' : 'max-cache-ttl',
+        'type' : LType.Uint64,
+        'default' : '86400',
+        'help' : 'maximum number of seconds to keep a cached entry in memory',
+        'doc' : '''
+Maximum number of seconds to cache an item in the DNS cache, no matter what the original TTL specified.
+This value also controls the refresh period of cached root data.
+See :ref:`handling-of-root-hints` for more information on this.
+ ''',
+     'versionchanged': ('4.1.0', 'The minimum value of this setting is 15. i.e. setting this to lower than 15 will make this value 15.')
+    },
+    {
+        'name' : 'max_concurrent_requests_per_tcp_connection',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '10',
+        'help' : 'Maximum number of requests handled concurrently per TCP connection',
+        'doc' : '''
+Maximum number of incoming requests handled concurrently per tcp
+connection. This number must be larger than 0 and smaller than 65536
+and also smaller than `max-mthreads`.
+ ''',
+    'versionadded': '4.3.0'
+    },
+    {
+        'name' : 'max_include_depth',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '20',
+        'help' : 'Maximum nested $INCLUDE depth when loading a zone from a file',
+        'doc' : '''
+Maximum number of nested ``$INCLUDE`` directives while processing a zone file.
+Zero mean no ``$INCLUDE`` directives will be accepted.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'max_generate_steps',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Maximum number of $GENERATE steps when loading a zone from a file',
+        'doc' : '''
+Maximum number of steps for a '$GENERATE' directive when parsing a
+zone file. This is a protection measure to prevent consuming a lot of
+CPU and memory when untrusted zones are loaded. Default to 0 which
+means unlimited.
+ ''',
+    'versionadded': '4.3.0'
+    },
+    {
+        'name' : 'max_mthreads',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '2048',
+        'help' : 'Maximum number of simultaneous Mtasker threads',
+        'doc' : '''
+Maximum number of simultaneous MTasker threads.
+ ''',
+    },
+    {
+        'name' : 'max_entries',
+        'section' : 'packetcache',
+        'oldname' : 'max-packetcache-entries',
+        'type' : LType.Uint64,
+        'default' : '500000',
+        'help' : 'maximum number of entries to keep in the packetcache',
+        'doc' : '''
+Maximum number of Packet Cache entries. Sharded and shared by all threads since 4.9.0.
+''',
+    },
+    {
+        'name' : 'max_qperq',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '60',
+        'help' : 'Maximum outgoing queries per query',
+        'doc' : '''
+The maximum number of outgoing queries that will be sent out during the resolution of a single client query.
+This is used to limit endlessly chasing CNAME redirections.
+If qname-minimization is enabled, the number will be forced to be 100
+at a minimum to allow for the extra queries qname-minimization generates when the cache is empty.
+ ''',
+    },
+    {
+        'name' : 'max_ns_address_qperq',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '10',
+        'help' : 'Maximum outgoing NS address queries per query',
+        'doc' : '''
+The maximum number of outgoing queries with empty replies for
+resolving nameserver names to addresses we allow during the resolution
+of a single client query. If IPv6 is enabled, an A and a AAAA query
+for a name counts as 1. If a zone publishes more than this number of
+NS records, the limit is further reduced for that zone by lowering
+it by the number of NS records found above the
+:ref:`setting-max-ns-address-qperq` value. The limit wil not be reduced to a
+number lower than 5.
+ ''',
+    'versionadded' : ['4.1.16', '4.2.2', '4.3.1']
+    },
+    {
+        'name' : 'max_ns_per_resolve',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '13',
+        'help' : 'Maximum number of NS records to consider to resolve a name, 0 is no limit',
+        'doc' : '''
+The maximum number of NS records that will be considered to select a nameserver to contact to resolve a name.
+If a zone has more than :ref:`setting-max-ns-per-resolve` NS records, a random sample of this size will be used.
+If :ref:`setting-max-ns-per-resolve` is zero, no limit applies.
+ ''',
+    'versionadded': ['4.8.0', '4.7.3', '4.6.4', '4.5.11']
+    },
+    {
+        'name' : 'max_negative_ttl',
+        'section' : 'recordcache',
+        'type' : LType.Uint64,
+        'default' : '3600',
+        'help' : 'maximum number of seconds to keep a negative cached entry in memory',
+        'doc' : '''
+A query for which there is authoritatively no answer is cached to quickly deny a record's existence later on, without putting a heavy load on the remote server.
+In practice, caches can become saturated with hundreds of thousands of hosts which are tried only once.
+This setting, which defaults to 3600 seconds, puts a maximum on the amount of time negative entries are cached.
+ ''',
+    },
+    {
+        'name' : 'max_recursion_depth',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '16',
+        'help' : 'Maximum number of internal recursion calls per query, 0 for unlimited',
+        'doc' : '''
+Total maximum number of internal recursion calls the server may use to answer a single query.
+0 means unlimited.
+The value of :ref:`setting-stack-size` should be increased together with this one to prevent the stack from overflowing.
+If :ref:`setting-qname-minimization` is enabled, the fallback code in case of a failing resolve is allowed an additional `max-recursion-depth/2`.
+ ''',
+     'versionchanged': [('4.1.0', 'Before 4.1.0, this settings was unlimited.'),
+                        ('4.9.0', "Before 4.9.0 this setting's default was 40 and the limit on ``CNAME`` chains (fixed at 16) acted as a bound on he recursion depth.")]
+    },
+    {
+        'name' : 'max_tcp_clients',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '128',
+        'help' : 'Maximum number of simultaneous TCP clients',
+        'doc' : '''
+Maximum number of simultaneous incoming TCP connections allowed.
+ ''',
+    },
+    {
+        'name' : 'max_tcp_per_client',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'If set, maximum number of TCP sessions per client (IP address)',
+        'doc' : '''
+Maximum number of simultaneous incoming TCP connections allowed per client (remote IP address).
+ 0 means unlimited.
+ ''',
+    },
+    {
+        'name' : 'max_tcp_queries_per_connection',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'If set, maximum number of TCP queries in a TCP connection',
+        'doc' : '''
+Maximum number of DNS queries in a TCP connection.
+0 means unlimited.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'max_total_msec',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '7000',
+        'help' : 'Maximum total wall-clock time per query in milliseconds, 0 for unlimited',
+        'doc' : '''
+Total maximum number of milliseconds of wallclock time the server may use to answer a single query.
+0 means unlimited.
+ ''',
+    },
+    {
+        'name' : 'max_udp_queries_per_round',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '10000',
+        'help' : 'Maximum number of UDP queries processed per recvmsg() round, before returning back to normal processing',
+        'doc' : '''
+Under heavy load the recursor might be busy processing incoming UDP queries for a long while before there is no more of these, and might therefore
+neglect scheduling new ``mthreads``, handling responses from authoritative servers or responding to :doc:`rec_control <manpages/rec_control.1>`
+requests.
+This setting caps the maximum number of incoming UDP DNS queries processed in a single round of looping on ``recvmsg()`` after being woken up by the multiplexer, before
+returning back to normal processing and handling other events.
+ ''',
+    'versionadded': '4.1.4'
+    },
+    {
+        'name' : 'minimum_ttl_override',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '1',
+        'help' : 'The minimum TTL',
+        'doc' : '''
+This setting artificially raises all TTLs to be at least this long.
+Setting this to a value greater than 1 technically is an RFC violation, but might improve performance a lot.
+Using a value of 0 impacts performance of TTL 0 records greatly, since it forces the recursor to contact
+authoritative servers each time a client requests them.
+Can be set at runtime using ``rec_control set-minimum-ttl 3600``.
+ ''',
+     'versionchanged': ('4.5.0', 'Old versions used default 0.')
+    },
+    {
+        'name' : 'tracking',
+        'section' : 'nod',
+        'oldname' : 'new-domain-tracking',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Track newly observed domains (i.e. never seen before).',
+        'doc' : '''
+Whether to track newly observed domains, i.e. never seen before. This
+is a probabilistic algorithm, using a stable bloom filter to store
+records of previously seen domains. When enabled for the first time,
+all domains will appear to be newly observed, so the feature is best
+left enabled for e.g. a week or longer before using the results. Note
+that this feature is optional and must be enabled at compile-time,
+thus it may not be available in all pre-built packages.
+If protobuf is enabled and configured, then the newly observed domain
+status will appear as a flag in Response messages.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'log',
+        'section' : 'nod',
+        'oldname' : 'new-domain-log',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Log newly observed domains.',
+        'doc' : '''
+If a newly observed domain is detected, log that domain in the
+recursor log file. The log line looks something like::
+
+ Jul 18 11:31:25 Newly observed domain nod=sdfoijdfio.com
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'lookup',
+        'section' : 'nod',
+        'oldname' : 'new-domain-lookup',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Perform a DNS lookup newly observed domains as a subdomain of the configured domain',
+        'doc' : '''
+If a domain is specified, then each time a newly observed domain is
+detected, the recursor will perform an A record lookup of '<newly
+observed domain>.<lookup domain>'. For example if 'new-domain-lookup'
+is configured as 'nod.powerdns.com', and a new domain 'xyz123.tv' is
+detected, then an A record lookup will be made for
+'xyz123.tv.nod.powerdns.com'. This feature gives a way to share the
+newly observed domain with partners, vendors or security teams. The
+result of the DNS lookup will be ignored by the recursor.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'db_size',
+        'section' : 'nod',
+        'oldname' : 'new-domain-db-size',
+        'type' : LType.Uint64,
+        'default' : '67108864',
+        'help' : 'Size of the DB used to track new domains in terms of number of cells. Defaults to 67108864',
+        'doc' : '''
+The default size of the stable bloom filter used to store previously
+observed domains is 67108864. To change the number of cells, use this
+setting. For each cell, the SBF uses 1 bit of memory, and one byte of
+disk for the persistent file.
+If there are already persistent files saved to disk, this setting will
+have no effect unless you remove the existing files.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'history_dir',
+        'section' : 'nod',
+        'oldname' : 'new-domain-history-dir',
+        'type' : LType.String,
+        'default' : '/usr/local/var/lib/pdns-recursor/nod',
+        'help' : 'Persist new domain tracking data here to persist between restarts',
+        'doc' : '''
+This setting controls which directory is used to store the on-disk
+cache of previously observed domains.
+
+The default depends on ``LOCALSTATEDIR`` when building the software.
+Usually this comes down to ``/var/lib/pdns-recursor/nod`` or ``/usr/local/var/lib/pdns-recursor/nod``).
+
+The newly observed domain feature uses a stable bloom filter to store
+a history of previously observed domains. The data structure is
+synchronized to disk every 10 minutes, and is also initialized from
+disk on startup. This ensures that previously observed domains are
+preserved across recursor restarts.
+If you change the new-domain-db-size setting, you must remove any files
+from this directory.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'whitelist',
+        'section' : 'nod',
+        'oldname' : 'new-domain-whitelist',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'List of domains (and implicitly all subdomains) which will never be considered a new domain (deprecated)',
+        'doc' : '',
+        'versionadded': '4.2.0',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-new-domain-ignore-list`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'ignore_list',
+        'section' : 'nod',
+        'oldname' : 'new-domain-ignore-list',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'List of domains (and implicitly all subdomains) which will never be considered a new domain',
+        'doc' : '''
+This setting is a list of all domains (and implicitly all subdomains)
+that will never be considered a new domain. For example, if the domain
+'xyz123.tv' is in the list, then 'foo.bar.xyz123.tv' will never be
+considered a new domain. One use-case for the ignore list is to never
+reveal details of internal subdomains via the new-domain-lookup
+feature.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'pb_tag',
+        'section' : 'nod',
+        'oldname' : 'new-domain-pb-tag',
+        'type' : LType.String,
+        'default' : 'pdns-nod',
+        'help' : 'If protobuf is configured, the tag to use for messages containing newly observed domains. Defaults to \'pdns-nod\'',
+        'doc' : '''
+If protobuf is configured, then this tag will be added to all protobuf response messages when
+a new domain is observed.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'network_timeout',
+        'section' : 'outgoing',
+        'type' : LType.Uint64,
+        'default' : '1500',
+        'help' : 'Wait this number of milliseconds for network i/o',
+        'doc' : '''
+Number of milliseconds to wait for a remote authoritative server to respond.
+ ''',
+    },
+    {
+        'name' : 'no_shuffle',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Don\'t change',
+        'doc' : 'SKIP',
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'non_resolving_ns_max_fails',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '5',
+        'help' : 'Number of failed address resolves of a nameserver to start throttling it, 0 is disabled',
+        'doc' : '''
+Number of failed address resolves of a nameserver name to start throttling it, 0 is disabled.
+Nameservers matching :ref:`setting-dont-throttle-names` will not be throttled.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'non_resolving_ns_throttle_time',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '60',
+        'help' : 'Number of seconds to throttle a nameserver with a name failing to resolve',
+        'doc' : '''
+Number of seconds to throttle a nameserver with a name failing to resolve.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'nothing_below_nxdomain',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : 'dnssec',
+        'help' : 'When an NXDOMAIN exists in cache for a name with fewer labels than the qname, send NXDOMAIN without doing a lookup (see RFC 8020)',
+        'doc' : '''
+- One of ``no``, ``dnssec``, ``yes``.
+
+The type of :rfc:`8020` handling using cached NXDOMAIN responses.
+This RFC specifies that NXDOMAIN means that the DNS tree under the denied name MUST be empty.
+When an NXDOMAIN exists in the cache for a shorter name than the qname, no lookup is done and an NXDOMAIN is sent to the client.
+
+For instance, when ``foo.example.net`` is negatively cached, any query
+matching ``*.foo.example.net`` will be answered with NXDOMAIN directly
+without consulting authoritative servers.
+
+``no``
+  No :rfc:`8020` processing is done.
+
+``dnssec``
+  :rfc:`8020` processing is only done using cached NXDOMAIN records that are
+  DNSSEC validated.
+
+``yes``
+  :rfc:`8020` processing is done using any non-Bogus NXDOMAIN record
+  available in the cache.
+ ''',
+    'versionadded': '4.3.0'
+    },
+    {
+        'name' : 'nsec3_max_iterations',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '150',
+        'help' : 'Maximum number of iterations allowed for an NSEC3 record',
+        'doc' : '''
+Maximum number of iterations allowed for an NSEC3 record.
+If an answer containing an NSEC3 record with more iterations is received, its DNSSEC validation status is treated as Insecure.
+ ''',
+        'versionadded': '4.1.0',
+        'versionchanged': ('4.5.2', 'Default is now 150, was 2500 before.')
+    },
+    {
+        'name' : 'ttl',
+        'section' : 'packetcache',
+        'oldname' : 'packetcache-ttl',
+        'type' : LType.Uint64,
+        'default' : '86400',
+        'help' : 'maximum number of seconds to keep a cached entry in packetcache',
+        'doc' : '''
+Maximum number of seconds to cache an item in the packet cache, no matter what the original TTL specified.
+ ''',
+        'versionchanged': ('4.9.0', 'The default was changed from 3600 (1 hour) to 86400 (24 hours).')
+    },
+    {
+        'name' : 'negative_ttl',
+        'section' : 'packetcache',
+        'oldname' : 'packetcache-negative-ttl',
+        'type' : LType.Uint64,
+        'default' : '60',
+        'help' : 'maximum number of seconds to keep a cached NxDomain or NoData entry in packetcache',
+        'doc' : '''
+Maximum number of seconds to cache an ``NxDomain`` or ``NoData`` answer in the packetcache.
+This setting's maximum is capped to :ref:`setting-packetcache-ttl`.
+i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-negative-ttl`` at the default will lower ``packetcache-negative-ttl`` to ``15``.
+ ''',
+    'versionadded': '4.9.0'
+    },
+    {
+        'name' : 'servfail_ttl',
+        'section' : 'packetcache',
+        'oldname' : 'packetcache-servfail-ttl',
+        'type' : LType.Uint64,
+        'default' : '60',
+        'help' : 'maximum number of seconds to keep a cached servfail entry in packetcache',
+        'doc' : '''
+Maximum number of seconds to cache an answer indicating a failure to resolve in the packet cache.
+Before version 4.6.0 only ``ServFail`` answers were considered as such. Starting with 4.6.0, all responses with a code other than ``NoError`` and ``NXDomain``, or without records in the answer and authority sections, are considered as a failure to resolve.
+Since 4.9.0, negative answers are handled separately from resolving failures.
+ ''',
+        'doc-rst' : '''
+        'versionchanged': ('4.0.0', "This setting's maximum is capped to :ref:`setting-packetcache-ttl`.
+    i.e. setting ``packetcache-ttl=15`` and keeping ``packetcache-servfail-ttl`` at the default will lower ``packetcache-servfail-ttl`` to ``15``.")
+ '''
+    },
+    {
+        'name' : 'shards',
+        'section' : 'packetcache',
+        'oldname' : 'packetcache-shards',
+        'type' : LType.Uint64,
+        'default' : '1024',
+        'help' : 'Number of shards in the packet cache',
+        'doc' : '''
+Sets the number of shards in the packet cache. If you have high contention as reported by ``packetcache-contented/packetcache-acquired``,
+you can try to enlarge this value or run with fewer threads.
+ ''',
+    'versionadded': '4.9.0'
+    },
+    {
+        'name' : 'pdns_distributes_queries',
+        'section' : 'incoming',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If PowerDNS itself should distribute queries over threads',
+        'doc' : '''
+If set, PowerDNS will use distinct threads to listen to client sockets and distribute that work to worker-threads using a hash of the query.
+This feature should maximize the cache hit ratio on versions before 4.9.0.
+To use more than one thread set :ref:`setting-distributor-threads` in version 4.2.0 or newer.
+Enabling should improve performance on systems where :ref:`setting-reuseport` does not have the effect of
+balancing the queries evenly over multiple worker threads.
+ ''',
+     'versionchanged': ('4.9.0', 'Default changed to ``no``, previously it was ``yes``.')
+    },
+    {
+        'name' : 'processes',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '1',
+        'help' : 'Launch this number of processes (EXPERIMENTAL, DO NOT CHANGE)',
+        'doc' : '''SKIP''',
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'protobuf_use_kernel_timestamp',
+        'section' : 'logging',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Compute the latency of queries in protobuf messages by using the timestamp set by the kernel when the query was received (when available)',
+        'doc' : '''
+Whether to compute the latency of responses in protobuf messages using the timestamp set by the kernel when the query packet was received (when available), instead of computing it based on the moment we start processing the query.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'proxy_protocol_from',
+        'section' : 'incoming',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'A Proxy Protocol header is only allowed from these subnets',
+        'doc' : '''
+Ranges that are required to send a Proxy Protocol version 2 header in front of UDP and TCP queries, to pass the original source and destination addresses and ports to the recursor, as well as custom values.
+Queries that are not prefixed with such a header will not be accepted from clients in these ranges. Queries prefixed by headers from clients that are not listed in these ranges will be dropped.
+
+Note that once a Proxy Protocol header has been received, the source address from the proxy header instead of the address of the proxy will be checked against the :ref:`setting-allow-from` ACL.
+
+The dnsdist docs have `more information about the PROXY protocol <https://dnsdist.org/advanced/passing-source-address.html#proxy-protocol>`_.
+ ''',
+    'versionadded': '4.4.0'
+    },
+    {
+        'name' : 'proxy_protocol_maximum_size',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '512',
+        'help' : 'The maximum size of a proxy protocol payload, including the TLV values',
+        'doc' : '''
+The maximum size, in bytes, of a Proxy Protocol payload (header, addresses and ports, and TLV values). Queries with a larger payload will be dropped.
+ ''',
+    'versionadded': '4.4.0'
+    },
+    {
+        'name' : 'public_suffix_list_file',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Path to the Public Suffix List file, if any',
+        'doc' : '''
+Path to the Public Suffix List file, if any. If set, PowerDNS will try to load the Public Suffix List from this file instead of using the built-in list. The PSL is used to group the queries by relevant domain names when displaying the top queries.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'qname_minimization',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Use Query Name Minimization',
+        'doc' : '''
+Enable Query Name Minimization. This implements a relaxed form of Query Name Mimimization as
+described in :rfc:`7816`.
+ ''',
+    'versionadded': '4.3.0'
+    },
+    {
+        'name' : 'source_address',
+        'section' : 'outgoing',
+        'oldname' : 'query-local-address',
+        'type' : LType.ListSubnets,
+        'default' : '0.0.0.0',
+        'help' : 'Source IP address for sending queries',
+        'doc' : '''
+Send out local queries from this address, or addresses. By adding multiple
+addresses, increased spoofing resilience is achieved. When no address of a certain
+address family is configured, there are *no* queries sent with that address family.
+In the default configuration this means that IPv6 is not used for outgoing queries.
+ ''',
+     'versionchanged': ('4.4.0', 'IPv6 addresses can be set with this option as well.')
+    },
+    {
+        'name' : 'quiet',
+        'section' : 'logging',
+        'type' : LType.Bool,
+        'default' : 'yes',
+        'help' : 'Suppress logging of questions and answers',
+        'doc' : '''
+Don't log queries.
+ ''',
+    },
+    {
+        'name' : 'locked_ttl_perc',
+        'section' : 'recordcache',
+        'oldname' : 'record-cache-locked-ttl-perc',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Replace records in record cache only after this % of original TTL has passed',
+        'doc' : '''
+Replace record sets in the record cache only after this percentage of the original TTL has passed.
+The PowerDNS Recursor already has several mechanisms to protect against spoofing attempts.
+This adds an extra layer of protection---as it limits the window of time cache updates are accepted---at the cost of a less efficient record cache.
+
+The default value of 0 means no extra locking occurs.
+When non-zero, record sets received (e.g. in the Additional Section) will not replace existing record sets in the record cache until the given percentage of the original TTL has expired.
+A value of 100 means only expired record sets will be replaced.
+
+There are a few cases where records will be replaced anyway:
+
+- Record sets that are expired will always be replaced.
+- Authoritative record sets will replace unauthoritative record sets unless DNSSEC validation of the new record set failed.
+- If the new record set belongs to a DNSSEC-secure zone and successfully passed validation it will replace an existing entry.
+- Record sets produced by :ref:`setting-refresh-on-ttl-perc` tasks will also replace existing record sets.
+ ''',
+    'versionadded': '4.8.0'
+    },
+    {
+        'name' : 'shards',
+        'section' : 'recordcache',
+        'oldname' : 'record-cache-shards',
+        'type' : LType.Uint64,
+        'default' : '1024',
+        'help' : 'Number of shards in the record cache',
+        'doc' : '''
+Sets the number of shards in the record cache. If you have high
+contention as reported by
+``record-cache-contented/record-cache-acquired``, you can try to
+enlarge this value or run with fewer threads.
+ ''',
+    'versionadded': '4.4.0'
+    },
+    {
+        'name' : 'refresh_on_ttl_perc',
+        'section' : 'recordcache',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'If a record is requested from the cache and only this % of original TTL remains, refetch',
+        'doc' : '''
+Sets the 'refresh almost expired' percentage of the record cache. Whenever a record is fetched from the packet or record cache
+and only ``refresh-on-ttl-perc`` percent or less of its original TTL is left, a task is queued to refetch the name/type combination to
+update the record cache. In most cases this causes future queries to always see a non-expired record cache entry.
+A typical value is 10. If the value is zero, this functionality is disabled.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'reuseport',
+        'section' : 'incoming',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Enable SO_REUSEPORT allowing multiple recursors processes to listen to 1 address',
+        'doc' : '''
+If ``SO_REUSEPORT`` support is available, allows multiple threads and processes to open listening sockets for the same port.
+
+Since 4.1.0, when :ref:`setting-pdns-distributes-queries` is disabled and :ref:`setting-reuseport` is enabled, every worker-thread will open a separate listening socket to let the kernel distribute the incoming queries instead of running a distributor thread (which could otherwise be a bottleneck) and avoiding thundering herd issues, thus leading to much higher performance on multi-core boxes.
+ ''',
+     'versionchanged': ('4.9.0', 'The default is changed to ``yes``, previously it was ``no``. If ``SO_REUSEPORT`` support is not available, the setting defaults to ``no``.')
+    },
+    {
+        'name' : 'rng',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : 'auto',
+        'help' : 'Specify random number generator to use. Valid values are auto,sodium,openssl,getrandom,arc4random,urandom.',
+        'doc' : '''
+- String
+- Default: auto
+
+Specify which random number generator to use. Permissible choices are
+ - auto - choose automatically
+ - sodium - Use libsodium ``randombytes_uniform``
+ - openssl - Use libcrypto ``RAND_bytes``
+ - getrandom - Use libc getrandom, falls back to urandom if it does not really work
+ - arc4random - Use BSD ``arc4random_uniform``
+ - urandom - Use ``/dev/urandom``
+ - kiss - Use simple settable deterministic RNG. **FOR TESTING PURPOSES ONLY!**
+ ''',
+    },
+    {
+        'name' : 'root_nx_trust',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'If set, believe that an NXDOMAIN from the root means the TLD does not exist',
+        'doc' : '''
+If set, an NXDOMAIN from the root-servers will serve as a blanket NXDOMAIN for the entire TLD the query belonged to.
+The effect of this is far fewer queries to the root-servers.
+ ''',
+     'versionchanged': ('4.0.0', "Default is ``yes`` now, was ``no`` before 4.0.0")
+    },
+    {
+        'name' : 'save_parent_ns_set',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Save parent NS set to be used if child NS set fails',
+        'doc' : '''
+If set, a parent (non-authoritative) ``NS`` set is saved if it contains more entries than a newly encountered child (authoritative) ``NS`` set for the same domain.
+The saved parent ``NS`` set is tried if resolution using the child ``NS`` set fails.
+ ''',
+    'versionadded': '4.7.0'
+    },
+    {
+        'name' : 'security_poll_suffix',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : 'secpoll.powerdns.com.',
+        'help' : 'Domain name from which to query security update notifications',
+        'doc' : '''
+Domain name from which to query security update notifications.
+Setting this to an empty string disables secpoll.
+ ''',
+    },
+    {
+        'name' : 'serve_rfc1918',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'If we should be authoritative for RFC 1918 private IP space',
+        'doc' : '''
+This makes the server authoritatively aware of: ``10.in-addr.arpa``, ``168.192.in-addr.arpa``, ``16-31.172.in-addr.arpa``, which saves load on the AS112 servers.
+Individual parts of these zones can still be loaded or forwarded.
+ ''',
+    },
+    {
+        'name' : 'serve_stale_extensions',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Number of times a record\'s ttl is extended by 30s to be served stale',
+        'doc' : '''
+Maximum number of times an expired record's TTL is extended by 30s when serving stale.
+Extension only occurs if a record cannot be refreshed.
+A value of 0 means the ``Serve Stale`` mechanism is not used.
+To allow records becoming stale to be served for an hour, use a value of 120.
+See :ref:`serve-stale` for a description of the Serve Stale mechanism.
+ ''',
+    'versionadded': '4.8.0'
+    },
+    {
+        'name' : 'server_down_max_fails',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '64',
+        'help' : 'Maximum number of consecutive timeouts (and unreachables) to mark a server as down ( 0 => disabled )',
+        'doc' : '''
+If a server has not responded in any way this many times in a row, no longer send it any queries for :ref:`setting-server-down-throttle-time` seconds.
+Afterwards, we will try a new packet, and if that also gets no response at all, we again throttle for :ref:`setting-server-down-throttle-time` seconds.
+Even a single response packet will drop the block.
+ ''',
+    },
+    {
+        'name' : 'server_down_throttle_time',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '60',
+        'help' : 'Number of seconds to throttle all queries to a server after being marked as down',
+        'doc' : '''
+Throttle a server that has failed to respond :ref:`setting-server-down-max-fails` times for this many seconds.
+ ''',
+    },
+    {
+        'name' : 'server_id',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : RUNTIME,
+        'help' : 'Returned when queried for \'id.server\' TXT or NSID, defaults to hostname, set custom or \'disabled\'',
+        'doc' : '''
+The reply given by The PowerDNS recursor to a query for 'id.server' with its hostname, useful for in clusters.
+When a query contains the :rfc:`NSID EDNS0 Option <5001>`, this value is returned in the response as the NSID value.
+
+This setting can be used to override the answer given to these queries.
+Set to 'disabled' to disable NSID and 'id.server' answers.
+
+Query example (where 192.0.2.14 is your server):
+
+.. code-block:: sh
+
+    dig @192.0.2.14 CHAOS TXT id.server.
+    dig @192.0.2.14 example.com IN A +nsid
+ ''',
+    },
+    {
+        'name' : 'setgid',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, change group id to this gid for more security',
+        'doc' : '''
+PowerDNS can change its user and group id after binding to its socket.
+Can be used for better :doc:`security <security>`. 
+ '''
+    },
+    {
+        'name' : 'setuid',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set, change user id to this uid for more security',
+        'doc' : '''
+PowerDNS can change its user and group id after binding to its socket.
+Can be used for better :doc:`security <security>`.
+ '''
+    },
+    {
+        'name' : 'signature_inception_skew',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '60',
+        'help' : 'Allow the signature inception to be off by this number of seconds',
+        'doc' : '''
+Allow the signature inception to be off by this number of seconds. Negative values are not allowed.
+ ''',
+        'versionadded': '4.1.5',
+        'versionchanged': ('4.2.0', 'Default is now 60, was 0 before.')
+    },
+    {
+        'name' : 'single_socket',
+        'section' : 'outgoing',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If set, only use a single socket for outgoing queries',
+        'doc' : '''
+Use only a single socket for outgoing queries.
+ ''',
+    },
+    {
+        'name' : 'agent',
+        'section' : 'snmp',
+        'oldname' : 'snmp-agent',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'If set, register as an SNMP agent',
+        'doc' : '''
+If set to true and PowerDNS has been compiled with SNMP support, it will register as an SNMP agent to provide statistics and be able to send traps.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'master_socket',
+        'section' : 'snmp',
+        'oldname' : 'snmp-master-socket',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set and snmp-agent is set, the socket to use to register to the SNMP daemon (deprecated)',
+        'doc' : '''
+ ''',
+        'versionadded': '4.1.0',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-snmp-daemon-socket`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'daemon_socket',
+        'section' : 'snmp',
+        'oldname' : 'snmp-daemon-socket',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'If set and snmp-agent is set, the socket to use to register to the SNMP daemon',
+        'doc' : '''
+If not empty and ``snmp-agent`` is set to true, indicates how PowerDNS should contact the SNMP daemon to register as an SNMP agent.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'soa_minimum_ttl',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Don\'t change',
+        'doc' : '''SKIP''',
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'socket_dir',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Where the controlsocket will live, /var/run/pdns-recursor when unset and not chrooted',
+        'doc' : '''
+Where to store the control socket and pidfile.
+The default depends on ``LOCALSTATEDIR`` or the ``--with-socketdir`` setting when building (usually ``/var/run`` or ``/run``).
+
+When using :ref:`setting-chroot` the default becomes ``/``.
+ ''',
+    },
+    {
+        'name' : 'socket_group',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Group of socket',
+        'doc' : '''
+Group and mode of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+'''
+    },
+    {
+        'name' : 'socket_mode',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Permissions for socket',
+        'doc' : '''
+Mode of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+ '''
+    },
+    {
+        'name' : 'socket_owner',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Owner of socket',
+        'doc' : '''
+Owner of the controlsocket.
+Owner and group can be specified by name, mode is in octal.
+ '''
+    },
+    {
+        'name' : 'spoof_nearmiss_max',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '1',
+        'help' : 'If non-zero, assume spoofing after this many near misses',
+        'doc' : '''
+If set to non-zero, PowerDNS will assume it is being spoofed after seeing this many answers with the wrong id.
+ ''',
+     'versionchanged': ('4.5.0', 'Older versions used 20 as the default value.')
+    },
+    {
+        'name' : 'stack_cache_size',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '100',
+        'help' : 'Size of the stack cache, per mthread',
+        'doc' : '''
+Maximum number of mthread stacks that can be cached for later reuse, per thread. Caching these stacks reduces the CPU load at the cost of a slightly higher memory usage, each cached stack consuming `stack-size` bytes of memory.
+It makes no sense to cache more stacks than the value of `max-mthreads`, since there will never be more stacks than that in use at a given time.
+ ''',
+    'versionadded': '4.9.0'
+    },
+    {
+        'name' : 'stack_size',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '200000',
+        'help' : 'stack size per mthread',
+        'doc' : '''
+Size in bytes of the stack of each mthread.
+ ''',
+    },
+    {
+        'name' : 'statistics_interval',
+        'section' : 'logging',
+        'type' : LType.Uint64,
+        'default' : '1800',
+        'help' : 'Number of seconds between printing of recursor statistics, 0 to disable',
+        'doc' : '''
+Interval between logging statistical summary on recursor performance.
+Use 0 to disable.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'stats_api_blacklist',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128',
+        'help' : 'List of statistics that are disabled when retrieving the complete list of statistics via the API (deprecated)',
+        'docdefault': '',
+        'doc' : '',
+        'versionadded': '4.2.0',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-stats-api-disabled-list`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'stats_api_disabled_list',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128',
+        'docdefault': 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*',
+        'help' : 'List of statistics that are disabled when retrieving the complete list of statistics via the API',
+        'doc' : '''
+A list of comma-separated statistic names, that are disabled when retrieving the complete list of statistics via the API for performance reasons.
+These statistics can still be retrieved individually by specifically asking for it.
+ ''',
+        'doc-new' : '''
+A sequence of statistic names, that are disabled when retrieving the complete list of statistics via the API for performance reasons.
+These statistics can still be retrieved individually by specifically asking for it.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'stats_carbon_blacklist',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128, cumul-clientanswers, cumul-authanswers, policy-hits, proxy-mapping-total, remote-logger-count',
+        'docdefault': '',
+        'help' : 'List of statistics that are prevented from being exported via Carbon (deprecated)',
+        'doc' : '',
+        'versionadded': '4.2.0',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-stats-carbon-disabled-list`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'stats_carbon_disabled_list',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128, cumul-clientanswers, cumul-authanswers, policy-hits, proxy-mapping-total, remote-logger-count',
+        'docdefault': 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*',
+        'help' : 'List of statistics that are prevented from being exported via Carbon',
+        'doc' : '''
+A list of comma-separated statistic names, that are prevented from being exported via carbon for performance reasons.
+ ''',
+        'doc-new' : '''
+A sequence of statistic names, that are prevented from being exported via carbon for performance reasons.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'stats_rec_control_blacklist',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128, cumul-clientanswers, cumul-authanswers, policy-hits, proxy-mapping-total, remote-logger-count',
+        'docdefault': '',
+        'help' : 'List of statistics that are prevented from being exported via rec_control get-all (deprecated)',
+        'doc' : '',
+        'versionadded': '4.2.0',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-stats-rec-control-disabled-list`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'stats_rec_control_disabled_list',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128, cumul-clientanswers, cumul-authanswers, policy-hits, proxy-mapping-total, remote-logger-count',
+        'docdefault': 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*, cumul-answers-\*, cumul-auth4answers-\*, cumul-auth6answers-\*',
+        'help' : 'List of statistics that are prevented from being exported via rec_control get-all',
+        'doc' : '''
+A list of comma-separated statistic names, that are disabled when retrieving the complete list of statistics via `rec_control get-all`, for performance reasons.
+These statistics can still be retrieved individually.
+ ''',
+        'doc-new' : '''
+A sequence of statistic names, that are disabled when retrieving the complete list of statistics via `rec_control get-all`, for performance reasons.
+These statistics can still be retrieved individually.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'stats_ringbuffer_entries',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '10000',
+        'help' : 'maximum number of packets to store statistics for',
+        'doc' : '''
+Number of entries in the remotes ringbuffer, which keeps statistics on who is querying your server.
+Can be read out using ``rec_control top-remotes``.
+ ''',
+    },
+    {
+        'name' : 'stats_snmp_blacklist',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128, cumul-clientanswers, cumul-authanswers, policy-hits, proxy-mapping-total, remote-logger-count',
+        'docdefault': '',
+        'help' : 'List of statistics that are prevented from being exported via SNMP (deprecated)',
+        'doc' : '',
+        'versionadded': '4.2.0',
+        'deprecated': ('4.5.0', 'Use :ref:`setting-stats-snmp-disabled-list`.'),
+        'skip-yaml': True,
+    },
+    {
+        'name' : 'stats_snmp_disabled_list',
+        'section' : 'recursor',
+        'type' : LType.ListStrings,
+        'default' : 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-1, ecs-v4-response-bits-2, ecs-v4-response-bits-3, ecs-v4-response-bits-4, ecs-v4-response-bits-5, ecs-v4-response-bits-6, ecs-v4-response-bits-7, ecs-v4-response-bits-8, ecs-v4-response-bits-9, ecs-v4-response-bits-10, ecs-v4-response-bits-11, ecs-v4-response-bits-12, ecs-v4-response-bits-13, ecs-v4-response-bits-14, ecs-v4-response-bits-15, ecs-v4-response-bits-16, ecs-v4-response-bits-17, ecs-v4-response-bits-18, ecs-v4-response-bits-19, ecs-v4-response-bits-20, ecs-v4-response-bits-21, ecs-v4-response-bits-22, ecs-v4-response-bits-23, ecs-v4-response-bits-24, ecs-v4-response-bits-25, ecs-v4-response-bits-26, ecs-v4-response-bits-27, ecs-v4-response-bits-28, ecs-v4-response-bits-29, ecs-v4-response-bits-30, ecs-v4-response-bits-31, ecs-v4-response-bits-32, ecs-v6-response-bits-1, ecs-v6-response-bits-2, ecs-v6-response-bits-3, ecs-v6-response-bits-4, ecs-v6-response-bits-5, ecs-v6-response-bits-6, ecs-v6-response-bits-7, ecs-v6-response-bits-8, ecs-v6-response-bits-9, ecs-v6-response-bits-10, ecs-v6-response-bits-11, ecs-v6-response-bits-12, ecs-v6-response-bits-13, ecs-v6-response-bits-14, ecs-v6-response-bits-15, ecs-v6-response-bits-16, ecs-v6-response-bits-17, ecs-v6-response-bits-18, ecs-v6-response-bits-19, ecs-v6-response-bits-20, ecs-v6-response-bits-21, ecs-v6-response-bits-22, ecs-v6-response-bits-23, ecs-v6-response-bits-24, ecs-v6-response-bits-25, ecs-v6-response-bits-26, ecs-v6-response-bits-27, ecs-v6-response-bits-28, ecs-v6-response-bits-29, ecs-v6-response-bits-30, ecs-v6-response-bits-31, ecs-v6-response-bits-32, ecs-v6-response-bits-33, ecs-v6-response-bits-34, ecs-v6-response-bits-35, ecs-v6-response-bits-36, ecs-v6-response-bits-37, ecs-v6-response-bits-38, ecs-v6-response-bits-39, ecs-v6-response-bits-40, ecs-v6-response-bits-41, ecs-v6-response-bits-42, ecs-v6-response-bits-43, ecs-v6-response-bits-44, ecs-v6-response-bits-45, ecs-v6-response-bits-46, ecs-v6-response-bits-47, ecs-v6-response-bits-48, ecs-v6-response-bits-49, ecs-v6-response-bits-50, ecs-v6-response-bits-51, ecs-v6-response-bits-52, ecs-v6-response-bits-53, ecs-v6-response-bits-54, ecs-v6-response-bits-55, ecs-v6-response-bits-56, ecs-v6-response-bits-57, ecs-v6-response-bits-58, ecs-v6-response-bits-59, ecs-v6-response-bits-60, ecs-v6-response-bits-61, ecs-v6-response-bits-62, ecs-v6-response-bits-63, ecs-v6-response-bits-64, ecs-v6-response-bits-65, ecs-v6-response-bits-66, ecs-v6-response-bits-67, ecs-v6-response-bits-68, ecs-v6-response-bits-69, ecs-v6-response-bits-70, ecs-v6-response-bits-71, ecs-v6-response-bits-72, ecs-v6-response-bits-73, ecs-v6-response-bits-74, ecs-v6-response-bits-75, ecs-v6-response-bits-76, ecs-v6-response-bits-77, ecs-v6-response-bits-78, ecs-v6-response-bits-79, ecs-v6-response-bits-80, ecs-v6-response-bits-81, ecs-v6-response-bits-82, ecs-v6-response-bits-83, ecs-v6-response-bits-84, ecs-v6-response-bits-85, ecs-v6-response-bits-86, ecs-v6-response-bits-87, ecs-v6-response-bits-88, ecs-v6-response-bits-89, ecs-v6-response-bits-90, ecs-v6-response-bits-91, ecs-v6-response-bits-92, ecs-v6-response-bits-93, ecs-v6-response-bits-94, ecs-v6-response-bits-95, ecs-v6-response-bits-96, ecs-v6-response-bits-97, ecs-v6-response-bits-98, ecs-v6-response-bits-99, ecs-v6-response-bits-100, ecs-v6-response-bits-101, ecs-v6-response-bits-102, ecs-v6-response-bits-103, ecs-v6-response-bits-104, ecs-v6-response-bits-105, ecs-v6-response-bits-106, ecs-v6-response-bits-107, ecs-v6-response-bits-108, ecs-v6-response-bits-109, ecs-v6-response-bits-110, ecs-v6-response-bits-111, ecs-v6-response-bits-112, ecs-v6-response-bits-113, ecs-v6-response-bits-114, ecs-v6-response-bits-115, ecs-v6-response-bits-116, ecs-v6-response-bits-117, ecs-v6-response-bits-118, ecs-v6-response-bits-119, ecs-v6-response-bits-120, ecs-v6-response-bits-121, ecs-v6-response-bits-122, ecs-v6-response-bits-123, ecs-v6-response-bits-124, ecs-v6-response-bits-125, ecs-v6-response-bits-126, ecs-v6-response-bits-127, ecs-v6-response-bits-128, cumul-clientanswers, cumul-authanswers, policy-hits, proxy-mapping-total, remote-logger-count',
+        'docdefault': 'cache-bytes, packetcache-bytes, special-memory-usage, ecs-v4-response-bits-\*, ecs-v6-response-bits-\*',
+        'help' : 'List of statistics that are prevented from being exported via SNMP',
+        'doc' : '''
+A list of comma-separated statistic names, that are prevented from being exported via SNMP, for performance reasons.
+ ''',
+        'doc-new' : '''
+A sequence of statistic names, that are prevented from being exported via SNMP, for performance reasons.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'structured_logging',
+        'section' : 'logging',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Prefer structured logging',
+        'doc' : '''
+Prefer structured logging when both an old style and a structured log messages is available.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'structured_logging_backend',
+        'section' : 'logging',
+        'type' : LType.String,
+        'default' : 'default',
+        'help' : 'Structured logging backend',
+        'doc' : '''
+The backend used for structured logging output.
+This setting must be set on the command line (``--structured-logging-backend=...``) to be effective.
+Available backends are:
+
+- ``default``: use the traditional logging system to output structured logging information.
+- ``systemd-journal``: use systemd-journal.
+  When using this backend, provide ``-o verbose`` or simular output option to ``journalctl`` to view the full information.
+ ''',
+    'versionadded': '4.8.0'
+    },
+    {
+        'name' : 'tcp_fast_open',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Enable TCP Fast Open support on the listening sockets, using the supplied numerical value as the queue size',
+        'doc' : '''
+Enable TCP Fast Open support, if available, on the listening sockets.
+The numerical value supplied is used as the queue size, 0 meaning disabled. See :ref:`tcp-fast-open-support`.
+ ''',
+    'versionadded': '4.1.0'
+    },
+    {
+        'name' : 'tcp_fast_open_connect',
+        'section' : 'outgoing',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Enable TCP Fast Open support on outgoing sockets',
+        'doc' : '''
+Enable TCP Fast Open Connect support, if available, on the outgoing connections to authoritative servers. See :ref:`tcp-fast-open-support`.
+ ''',
+    'versionadded': '4.5.0'
+    },
+    {
+        'name' : 'tcp_max_idle_ms',
+        'section' : 'outgoing',
+        'oldname' : 'tcp-out-max-idle-ms',
+        'type' : LType.Uint64,
+        'default' : '10000',
+        'help' : 'Time TCP/DoT connections are left idle in milliseconds or 0 if no limit',
+        'doc' : '''
+Time outgoing TCP/DoT connections are left idle in milliseconds or 0 if no limit. After having been idle for this time, the connection is eligible for closing.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'tcp_max_idle_per_auth',
+        'section' : 'outgoing',
+        'oldname' : 'tcp-out-max-idle-per-auth',
+        'type' : LType.Uint64,
+        'default' : '10',
+        'help' : 'Maximum number of idle TCP/DoT connections to a specific IP per thread, 0 means do not keep idle connections open',
+        'doc' : '''
+Maximum number of idle outgoing TCP/DoT connections to a specific IP per thread, 0 means do not keep idle connections open.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'tcp_max_queries',
+        'section' : 'outgoing',
+        'oldname' : 'tcp-out-max-queries',
+        'type' : LType.Uint64,
+        'default' : '0',
+        'help' : 'Maximum total number of queries per TCP/DoT connection, 0 means no limit',
+        'doc' : '''
+Maximum total number of queries per outgoing TCP/DoT connection, 0 means no limit. After this number of queries, the connection is
+closed and a new one will be created if needed.
+ ''',
+    },
+    {
+        'name' : 'tcp_max_idle_per_thread',
+        'section' : 'outgoing',
+        'oldname' : 'tcp-out-max-idle-per-thread',
+        'type' : LType.Uint64,
+        'default' : '100',
+        'help' : 'Maximum number of idle TCP/DoT connections per thread',
+        'doc' : '''
+Maximum number of idle outgoing TCP/DoT connections per thread, 0 means do not keep idle connections open.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'threads',
+        'section' : 'recursor',
+        'type' : LType.Uint64,
+        'default' : '2',
+        'help' : 'Launch this number of threads',
+        'doc' : '''
+Spawn this number of threads on startup.
+ ''',
+    },
+    {
+        'name' : 'trace',
+        'section' : 'logging',
+        'type' : LType.String,
+        'default' : 'no',
+        'help' : 'if we should output heaps of logging. set to \'fail\' to only log failing domains',
+        'doc' : '''
+One of ``no``, ``yes`` or ``fail``.
+If turned on, output impressive heaps of logging.
+May destroy performance under load.
+To log only queries resulting in a ``ServFail`` answer from the resolving process, this value can be set to ``fail``, but note that the performance impact is still large.
+Also note that queries that do produce a result but with a failing DNSSEC validation are not written to the log
+ ''',
+    },
+    {
+        'name' : 'udp_source_port_min',
+        'section' : 'outgoing',
+        'type' : LType.Uint64,
+        'default' : '1024',
+        'help' : 'Minimum UDP port to bind on',
+        'doc' : '''
+This option sets the low limit of UDP port number to bind on.
+
+In combination with :ref:`setting-udp-source-port-max` it configures the UDP
+port range to use. Port numbers are randomized within this range on
+initialization, and exceptions can be configured with :ref:`setting-udp-source-port-avoid`
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'udp_source_port_max',
+        'section' : 'outgoing',
+        'type' : LType.Uint64,
+        'default' : '65535',
+        'help' : 'Maximum UDP port to bind on',
+        'doc' : '''
+This option sets the maximum limit of UDP port number to bind on.
+
+See :ref:`setting-udp-source-port-min`.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'udp_source_port_avoid',
+        'section' : 'outgoing',
+        'type' : LType.ListStrings,
+        'default' : '11211',
+        'help' : 'List of comma separated UDP port number to avoid',
+        'doc' : '''
+A list of comma-separated UDP port numbers to avoid when binding.
+Ex: `5300,11211`
+
+See :ref:`setting-udp-source-port-min`.
+ ''',
+        'doc-new' : '''
+A sequence of UDP port numbers to avoid when binding. For example:
+
+.. code-block:: yaml
+
+ outgoing:
+   udp_source_port_avoid:
+   - 5300
+   - 11211
+
+See :ref:`setting-udp-source-port-min`.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'udp_truncation_threshold',
+        'section' : 'incoming',
+        'type' : LType.Uint64,
+        'default' : '1232',
+        'help' : 'Maximum UDP response size before we truncate',
+        'doc' : '''
+EDNS0 allows for large UDP response datagrams, which can potentially raise performance.
+Large responses however also have downsides in terms of reflection attacks.
+This setting limits the accepted size.
+Maximum value is 65535, but values above 4096 should probably not be attempted.
+
+To know why 1232, see the note at :ref:`setting-edns-outgoing-bufsize`.
+ ''',
+        'versionchanged': ('4.2.0', 'Before 4.2.0, the default was 1680.')
+    },
+    {
+        'name' : 'unique_response_tracking',
+        'section' : 'nod',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Track unique responses (tuple of query name, type and RR).',
+        'doc' : '''
+Whether to track unique DNS responses, i.e. never seen before combinations
+of the triplet (query name, query type, RR[rrname, rrtype, rrdata]).
+This can be useful for tracking potentially suspicious domains and
+behaviour, e.g. DNS fast-flux.
+If protobuf is enabled and configured, then the Protobuf Response message
+will contain a flag with udr set to true for each RR that is considered
+unique, i.e. never seen before.
+This feature uses a probabilistic data structure (stable bloom filter) to
+track unique responses, which can have false positives as well as false
+negatives, thus it is a best-effort feature. Increasing the number of cells
+in the SBF using the unique-response-db-size setting can reduce FPs and FNs.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'unique_response_log',
+        'section' : 'nod',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Log unique responses',
+        'doc' : '''
+Whether to log when a unique response is detected. The log line
+looks something like:
+
+Oct 24 12:11:27 Unique response observed: qname=foo.com qtype=A rrtype=AAAA rrname=foo.com rrcontent=1.2.3.4
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'unique_response_db_size',
+        'section' : 'nod',
+        'type' : LType.Uint64,
+        'default' : '67108864',
+        'help' : 'Size of the DB used to track unique responses in terms of number of cells. Defaults to 67108864',
+        'doc' : '''
+The default size of the stable bloom filter used to store previously
+observed responses is 67108864. To change the number of cells, use this
+setting. For each cell, the SBF uses 1 bit of memory, and one byte of
+disk for the persistent file.
+If there are already persistent files saved to disk, this setting will
+have no effect unless you remove the existing files.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'unique_response_history_dir',
+        'section' : 'nod',
+        'type' : LType.String,
+        'default' : '/usr/local/var/lib/pdns-recursor/udr',
+        'help' : 'Persist unique response tracking data here to persist between restarts',
+        'doc' : '''
+This setting controls which directory is used to store the on-disk
+cache of previously observed responses.
+
+The default depends on ``LOCALSTATEDIR`` when building the software.
+Usually this comes down to ``/var/lib/pdns-recursor/udr`` or ``/usr/local/var/lib/pdns-recursor/udr``).
+
+The newly observed domain feature uses a stable bloom filter to store
+a history of previously observed responses. The data structure is
+synchronized to disk every 10 minutes, and is also initialized from
+disk on startup. This ensures that previously observed responses are
+preserved across recursor restarts. If you change the
+unique-response-db-size, you must remove any files from this directory.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'unique_response_pb_tag',
+        'section' : 'nod',
+        'type' : LType.String,
+        'default' : 'pdns-udr',
+        'help' : 'If protobuf is configured, the tag to use for messages containing unique DNS responses. Defaults to \'pdns-udr\'',
+        'doc' : '''
+If protobuf is configured, then this tag will be added to all protobuf response messages when
+a unique DNS response is observed.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'use_incoming_edns_subnet',
+        'section' : 'incoming',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Pass along received EDNS Client Subnet information',
+        'doc' : '''
+Whether to process and pass along a received EDNS Client Subnet to authoritative servers.
+The ECS information will only be sent for netmasks and domains listed in :ref:`setting-edns-subnet-allow-list` and will be truncated if the received scope exceeds :ref:`setting-ecs-ipv4-bits` for IPv4 or :ref:`setting-ecs-ipv6-bits` for IPv6.
+ ''',
+    },
+    {
+        'name' : 'version',
+        'section' : 'commands',
+        'type' : LType.Command,
+        'default' : 'no',
+        'help' : 'Print version string',
+        'doc' : '''
+Print version of this binary. Useful for checking which version of the PowerDNS recursor is installed on a system.
+ ''',
+    },
+    {
+        'name' : 'version_string',
+        'section' : 'recursor',
+        'type' : LType.String,
+        'default' : RUNTIME,
+        'help' : 'string reported on version.pdns or version.bind',
+        'doc' : '''
+By default, PowerDNS replies to the 'version.bind' query with its version number.
+Security conscious users may wish to override the reply PowerDNS issues.
+ ''',
+    },
+    {
+        'name' : 'webserver',
+        'section' : 'webservice',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Start a webserver (for REST API)',
+        'doc' : '''
+Start the webserver (for REST API).
+ ''',
+    },
+    {
+        'name' : 'address',
+        'section' : 'webservice',
+        'oldname' : 'webserver-address',
+        'type' : LType.String,
+        'default' : '127.0.0.1',
+        'help' : 'IP Address of webserver to listen on',
+        'doc' : '''
+IP address for the webserver to listen on.
+ ''',
+    },
+    {
+        'name' : 'allow_from',
+        'section' : 'webservice',
+        'oldname' : 'webserver-allow-from',
+        'type' : LType.ListSubnets,
+        'default' : '127.0.0.1, ::1',
+        'help' : 'Webserver access is only allowed from these subnets',
+        'doc' : '''
+These IPs and subnets are allowed to access the webserver. Note that
+specifying an IP address without a netmask uses an implicit netmask
+of /32 or /128.
+ ''',
+        'versionchanged': ('4.1.0', 'Default is now 127.0.0.1,::1, was 0.0.0.0/0,::/0 before.')
+    },
+    {
+        'name' : 'hash_plaintext_credentials',
+        'section' : 'webservice',
+        'oldname': 'webserver-hash-plaintext-credentials',
+        'type' : LType.Bool,
+        'default' : 'false',
+        'help' : 'Whether to hash passwords and api keys supplied in plaintext, to prevent keeping the plaintext version in memory at runtime',
+        'doc' : '''
+Whether passwords and API keys supplied in the configuration as plaintext should be hashed during startup, to prevent the plaintext versions from staying in memory. Doing so increases significantly the cost of verifying credentials and is thus disabled by default.
+Note that this option only applies to credentials stored in the configuration as plaintext, but hashed credentials are supported without enabling this option.
+ ''',
+    'versionadded': '4.6.0'
+    },
+    {
+        'name' : 'loglevel',
+        'section' : 'webservice',
+        'oldname' : 'webserver-loglevel',
+        'type' : LType.String,
+        'default' : 'normal',
+        'help' : 'Amount of logging in the webserver (none, normal, detailed)',
+        'doc' : '''
+One of ``one``, ``normal``, ``detailed``.
+The amount of logging the webserver must do. 'none' means no useful webserver information will be logged.
+When set to 'normal', the webserver will log a line per request that should be familiar::
+
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 'GET /api/v1/servers/localhost/bla HTTP/1.1' 404 196
+
+When set to 'detailed', all information about the request and response are logged::
+
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e Request Details:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Headers:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   accept-encoding: gzip, deflate
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   accept-language: en-US,en;q=0.5
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   connection: keep-alive
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   dnt: 1
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   host: 127.0.0.1:8081
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   upgrade-insecure-requests: 1
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  No body
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e Response details:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Headers:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Connection: close
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Content-Length: 49
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Content-Type: text/html; charset=utf-8
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   Server: PowerDNS/0.0.15896.0.gaba8bab3ab
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e  Full body:
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e   <!html><title>Not Found</title><h1>Not Found</h1>
+  [webserver] e235780e-a5cf-415e-9326-9d33383e739e 127.0.0.1:55376 'GET /api/v1/servers/localhost/bla HTTP/1.1' 404 196
+
+The value between the hooks is a UUID that is generated for each request. This can be used to find all lines related to a single request.
+
+.. note::
+  The webserver logs these line on the NOTICE level. The :ref:`setting-loglevel` seting must be 5 or higher for these lines to end up in the log.
+ ''',
+    'versionadded': '4.2.0'
+    },
+    {
+        'name' : 'password',
+        'section' : 'webservice',
+        'oldname' : 'webserver-password',
+        'type' : LType.String,
+        'default' : '',
+        'help' : 'Password required for accessing the webserver',
+        'doc' : '''
+Password required to access the webserver. Since 4.6.0 the password can be hashed and salted using ``rec_control hash-password`` instead of being present in the configuration in plaintext, but the plaintext version is still supported.
+ ''',
+        'versionchanged': ('4.6.0', 'This setting now accepts a hashed and salted version.')
+    },
+    {
+        'name' : 'port',
+        'section' : 'webservice',
+        'type' : LType.Uint64,
+        'oldname': 'webserver-port',
+        'default' : '8082',
+        'help' : 'Port of webserver to listen on',
+        'doc' : '''
+TCP port where the webserver should listen on.
+ ''',
+    },
+    {
+        'name' : 'write_pid',
+        'section' : 'recursor',
+        'type' : LType.Bool,
+        'default' : 'true',
+        'help' : 'Write a PID file',
+        'doc' : '''
+If a PID file should be written to :ref:`setting-socket-dir`
+ ''',
+    },
+    {
+        'name' : 'x_dnssec_names',
+        'section' : 'dnssec',
+        'type' : LType.ListStrings,
+        'default' : '',
+        'help' : 'Collect DNSSEC statistics for names or suffixes in this list in separate x-dnssec counters',
+        'doc' : '''
+List of names whose DNSSEC validation metrics will be counted in a separate set of metrics that start
+with ``x-dnssec-result-``.
+The names are suffix-matched.
+This can be used to not count known failing (test) name validations in the ordinary DNSSEC metrics.
+ ''',
+    'versionadded': '4.5.0'
+    },
+]
index 9e8b622e20b133f43d2a281706dd68e4258598c6..9d40aa52fb75257bdaec20ef5ee3d9e5350fa766 100644 (file)
@@ -904,7 +904,7 @@ extern uint16_t g_outgoingEDNSBufsize;
 extern std::atomic<uint32_t> g_maxCacheEntries, g_maxPacketCacheEntries;
 extern bool g_lowercaseOutgoing;
 
-std::string reloadZoneConfiguration();
+std::string reloadZoneConfiguration(bool yaml);
 typedef std::function<void*(void)> pipefunc_t;
 void broadcastFunction(const pipefunc_t& func);
 void distributeAsyncFunction(const std::string& packet, const pipefunc_t& func);
@@ -919,7 +919,7 @@ template <class T>
 T broadcastAccFunction(const std::function<T*()>& func);
 
 typedef std::unordered_set<DNSName> notifyset_t;
-std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>> parseZoneConfiguration();
+std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>> parseZoneConfiguration(bool yaml);
 void* pleaseSupplantAllowNotifyFor(std::shared_ptr<notifyset_t> allowNotifyFor);
 
 uint64_t* pleaseGetNsSpeedsSize();
diff --git a/pdns/recursordist/test-settings.cc b/pdns/recursordist/test-settings.cc
new file mode 100644 (file)
index 0000000..779684e
--- /dev/null
@@ -0,0 +1,422 @@
+#define BOOST_TEST_DYN_LINK
+#include <boost/test/unit_test.hpp>
+
+#include <memory>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/format.hpp>
+
+#include "settings/cxxsettings.hh"
+
+BOOST_AUTO_TEST_SUITE(test_settings)
+
+BOOST_AUTO_TEST_CASE(test_rust_empty)
+{
+  const std::string yaml = "{}\n";
+  auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  // Check an attribute to see if it has the right default value
+  BOOST_CHECK_EQUAL(settings.dnssec.aggressive_nsec_cache_size, 100000U);
+
+  // Generate yaml, should be empty as all values are default
+  auto back = settings.to_yaml_string();
+  // rust::String does not play nice with BOOST_CHECK_EQUAL, it lacks a <<
+  BOOST_CHECK_EQUAL(yaml, std::string(back));
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_syntaxerror)
+{
+  const std::string yaml = "{incoming: port: \n";
+  BOOST_CHECK_THROW(pdns::rust::settings::rec::parse_yaml_string(yaml), rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_unknown_section)
+{
+  const std::string yaml = "{adskldsaj: port: \n";
+  BOOST_CHECK_THROW(pdns::rust::settings::rec::parse_yaml_string(yaml), rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_unknown_field)
+{
+  const std::string yaml = "{incoming: akajkj0: \n";
+  BOOST_CHECK_THROW(pdns::rust::settings::rec::parse_yaml_string(yaml), rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_parse)
+{
+  const std::string yaml = R"EOT(dnssec:
+  aggressive_nsec_cache_size: 10
+incoming:
+  allow_from:
+  - '!123.123.123.123'
+  - ::1
+recursor:
+  auth_zones:
+  - zone: example.com
+    file: a/file/name
+  - zone: example.net
+    file: another/file/name
+  forward_zones:
+  - zone: .
+    forwarders:
+    - 9.9.9.9
+  forward_zones_recurse:
+  - zone: .
+    forwarders:
+    - 9.9.9.9
+    - 1.2.3.4
+    - ::99
+    recurse: true
+webservice:
+  api_key: otto
+packetcache:
+  disable: true
+)EOT";
+
+  auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  BOOST_CHECK_EQUAL(settings.dnssec.aggressive_nsec_cache_size, 10U);
+  BOOST_CHECK_EQUAL(settings.incoming.allow_from.size(), 2U);
+  BOOST_REQUIRE_EQUAL(settings.recursor.auth_zones.size(), 2U);
+  BOOST_REQUIRE_EQUAL(settings.recursor.forward_zones.size(), 1U);
+  BOOST_REQUIRE_EQUAL(settings.recursor.forward_zones[0].forwarders.size(), 1U);
+  BOOST_REQUIRE_EQUAL(settings.recursor.forward_zones_recurse.size(), 1U);
+  BOOST_REQUIRE_EQUAL(settings.recursor.forward_zones_recurse[0].forwarders.size(), 3U);
+  BOOST_CHECK(settings.recursor.forward_zones_recurse[0].recurse);
+  auto back = settings.to_yaml_string();
+  // rust::String does not play nice with BOOST_CHECK_EQUAL, it lacks a <<
+  BOOST_CHECK_EQUAL(yaml, std::string(back));
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_validation_with_error1)
+{
+  const std::string yaml = R"EOT(
+incoming:
+  allow_from: ["1.2.3.8999"]
+)EOT";
+
+  BOOST_CHECK_THROW({
+    auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+    auto back = settings.to_yaml_string();
+    settings.validate();
+  },
+                    rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_validation_with_error2)
+{
+  const std::string yaml = R"EOT(
+recursor:
+  forward_zones:
+    - zone: "example.com"
+      forwarders:
+        - 1.2.3.4
+        - a.b
+)EOT";
+
+  auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  auto back = settings.to_yaml_string();
+  BOOST_CHECK_THROW({
+    settings.validate();
+  },
+                    rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_validation_with_error3)
+{
+  const std::string yaml = R"EOT(
+recursor:
+  forward_zones:
+    - zone:
+      forwarders:
+        - 1.2.3.4
+)EOT";
+
+  BOOST_CHECK_THROW({
+    auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+    auto back = settings.to_yaml_string();
+    settings.validate();
+  },
+                    rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_validation_with_error4)
+{
+  const std::string yaml = R"EOT(
+recursor:
+  forward_zones:
+    - zone: ok
+)EOT";
+
+  BOOST_CHECK_THROW({
+    auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+    auto back = settings.to_yaml_string();
+    settings.validate();
+  },
+                    rust::Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_validation_with_error5)
+{
+  const std::string yaml = R"EOT(
+recursor:
+  auth_zones:
+    - zone: %1%
+      file: filename
+)EOT";
+
+  const vector<string> oktests = {
+    ".",
+    "one",
+    "one.",
+    "two.label"
+    "two.label.",
+  };
+  for (const auto& ok : oktests) {
+    auto yamltest = boost::format(yaml) % ok;
+    BOOST_CHECK_NO_THROW({
+      auto settings = pdns::rust::settings::rec::parse_yaml_string(yamltest.str());
+      settings.validate();
+    });
+  }
+  const vector<string> noktests = {
+    "",
+    "..",
+    "two..label",
+    ".two.label",
+    "three€.a.label",
+    "three.a.label..",
+  };
+  for (const auto& nok : noktests) {
+    auto yamltest = boost::format(yaml) % nok;
+    BOOST_CHECK_THROW({
+      auto settings = pdns::rust::settings::rec::parse_yaml_string(yamltest.str());
+      auto back = settings.to_yaml_string();
+      settings.validate();
+    },
+                      rust::Error);
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_validation_no_error)
+{
+  // All defaults
+  const std::string yaml = "{}\n";
+
+  BOOST_CHECK_NO_THROW({
+    auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);
+    settings.validate();
+  });
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_forwardzones_to_yaml)
+{
+  using pdns::rust::settings::rec::ForwardZone;
+  rust::Vec<ForwardZone> vec;
+  vec.emplace_back(ForwardZone{"zone1", {"1.2.3.4"}, false, false});
+  vec.emplace_back(ForwardZone{"zone2", {"1.2.3.4", "::1"}, true, true});
+
+  auto yaml = pdns::rust::settings::rec::forward_zones_to_yaml_string(vec);
+
+  const std::string expected = R"EOT(- zone: zone1
+  forwarders:
+  - 1.2.3.4
+- zone: zone2
+  forwarders:
+  - 1.2.3.4
+  - ::1
+  recurse: true
+  notify_allowed: true
+)EOT";
+
+  BOOST_CHECK_EQUAL(std::string(yaml), expected);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_parse_forwardzones_to_yaml)
+{
+  std::string fileContent = R"EOT(
+# aap
+example1.com= 1.2.3.4, 5.6.7.8; 8.9.0.1
+^+example2.com = ::1
+)EOT";
+
+  const std::string expected = R"EOT(- zone: example1.com
+  forwarders:
+  - 1.2.3.4
+  - 5.6.7.8
+  - 8.9.0.1
+- zone: example2.com
+  forwarders:
+  - ::1
+  recurse: true
+  notify_allowed: true
+)EOT";
+
+  std::string temp("/tmp/test-settingsXXXXXXXXXX");
+  int fileDesc = mkstemp(temp.data());
+  BOOST_REQUIRE(fileDesc > 0);
+  auto filePtr = std::unique_ptr<FILE, decltype(&fclose)>(fdopen(fileDesc, "w"), fclose);
+  BOOST_REQUIRE(filePtr != nullptr);
+  size_t written = fwrite(fileContent.data(), 1, fileContent.length(), filePtr.get());
+  BOOST_REQUIRE(written == fileContent.length());
+  filePtr = nullptr;
+
+  rust::Vec<pdns::rust::settings::rec::ForwardZone> forwards;
+  pdns::settings::rec::oldStyleForwardsFileToBridgeStruct(temp, forwards);
+  unlink(temp.data());
+
+  auto yaml = pdns::rust::settings::rec::forward_zones_to_yaml_string(forwards);
+  BOOST_CHECK_EQUAL(std::string(yaml), expected);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_defaults)
+{
+  const std::string yaml = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  pdns::rust::settings::rec::merge(lhs, yaml);
+  auto back = lhs.to_yaml_string();
+  BOOST_CHECK_EQUAL(yaml, std::string(back));
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_lhs_default)
+{
+  const std::string yaml = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  auto rhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  const std::string yaml2 = R"EOT(
+recursor:
+  forward_zones:
+  - zone: zone
+    forwarders:
+    - 1.2.3.4
+dnssec:
+  validation: validate
+)EOT";
+
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(std::string(lhs.dnssec.validation), "validate");
+  BOOST_CHECK_EQUAL(lhs.recursor.forward_zones.size(), 1U);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_lhs_nondefault)
+{
+  const std::string yaml = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  auto rhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  lhs.dnssec.validation = "no";
+  lhs.recursor.forward_zones.emplace_back(pdns::rust::settings::rec::ForwardZone{"zone1", {"1.2.3.4"}, false, false});
+
+  rhs.dnssec.validation = "validate";
+  rhs.recursor.forward_zones.emplace_back(pdns::rust::settings::rec::ForwardZone{"zone2", {"1.2.3.4"}, false, false});
+
+  const auto yaml2 = rhs.to_yaml_string();
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(std::string(lhs.dnssec.validation), "validate");
+  BOOST_CHECK_EQUAL(lhs.recursor.forward_zones.size(), 2U);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_rhs_mixed)
+{
+  const std::string yaml = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  auto rhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  lhs.dnssec.validation = "no";
+  lhs.recursor.forward_zones.emplace_back(pdns::rust::settings::rec::ForwardZone{"zone1", {"1.2.3.4"}, false, false});
+  rhs.recursor.forward_zones.emplace_back(pdns::rust::settings::rec::ForwardZone{"zone2", {"1.2.3.4"}, false, false});
+
+  const auto yaml2 = rhs.to_yaml_string();
+  pdns::rust::settings::rec::merge(lhs, yaml);
+
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(std::string(lhs.dnssec.validation), "no");
+  BOOST_CHECK_EQUAL(lhs.recursor.forward_zones.size(), 2U);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_list_nonempty_default1)
+{
+  const std::string yaml = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  auto rhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  // Note that dont_query is a non-empty list by default
+  // lhs default, rhs is not (empty ), rhs overwrites lhs
+  rhs.outgoing.dont_query = {};
+  const auto yaml2 = rhs.to_yaml_string();
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(lhs.outgoing.dont_query.size(), 0U);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_list_nonempty_default2)
+{
+  const std::string yaml = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  auto rhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+
+  rhs.outgoing.dont_query = {"1.2.3.4"};
+  // lhs default, rhs overwrites lhs
+  const auto yaml2 = rhs.to_yaml_string();
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(lhs.outgoing.dont_query.size(), 1U);
+
+  rhs = pdns::rust::settings::rec::parse_yaml_string(yaml);
+  rhs.outgoing.dont_query = {"4.5.6.7"};
+  // lhs not default, rhs gets merged
+  const auto yaml3 = rhs.to_yaml_string();
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(lhs.outgoing.dont_query.size(), 2U);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_nondefault_and_default)
+{
+  const std::string yaml1 = "{}\n";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml1);
+  lhs.recordcache.max_entries = 99;
+  lhs.dnssec.validation = "no";
+  const std::string yaml2 = R"EOT(
+  dnssec:
+    validation: process
+  incoming:
+    allow_from:
+    - 4.5.6.7/1
+)EOT";
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(lhs.dnssec.validation, "process");
+  BOOST_CHECK_EQUAL(lhs.incoming.allow_from.size(), 1U);
+  BOOST_CHECK_EQUAL(lhs.recordcache.max_entries, 99U);
+}
+
+BOOST_AUTO_TEST_CASE(test_rust_merge_override)
+{
+  const std::string yaml1 = R"EOT(
+  incoming:
+    allow_from:
+    - 4.5.6.7/1
+)EOT";
+  auto lhs = pdns::rust::settings::rec::parse_yaml_string(yaml1);
+  lhs.recordcache.max_entries = 99;
+  lhs.dnssec.validation = "no";
+  const std::string yaml2 = R"EOT(
+  dnssec:
+    validation: process
+  incoming:
+    allow_from: !override
+    - 1.2.3.4/1
+)EOT";
+  pdns::rust::settings::rec::merge(lhs, yaml2);
+
+  BOOST_CHECK_EQUAL(lhs.dnssec.validation, "process");
+  BOOST_REQUIRE_EQUAL(lhs.incoming.allow_from.size(), 1U);
+  BOOST_CHECK_EQUAL(lhs.incoming.allow_from.at(0), "1.2.3.4/1");
+  BOOST_CHECK_EQUAL(lhs.recordcache.max_entries, 99U);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
index 98e420bb27164b53d4cc1bcf118e113210645c93..2737f78ec02d8bdfbb52680b558f89742c6685fe 100644 (file)
@@ -45,6 +45,7 @@
 #include "uuid-utils.hh"
 #include "tcpiohandler.hh"
 #include "rec-main.hh"
+#include "settings/cxxsettings.hh"
 
 using json11::Json;
 
@@ -69,8 +70,14 @@ static void apiWriteConfigFile(const string& filebasename, const string& content
     throw ApiException("Config Option \"api-config-dir\" must be set");
   }
 
-  string filename = ::arg()["api-config-dir"] + "/" + filebasename + ".conf";
-  ofstream ofconf(filename.c_str());
+  string filename = ::arg()["api-config-dir"] + "/" + filebasename;
+  if (g_yamlSettings) {
+    filename += ".yml";
+  }
+  else {
+    filename += ".conf";
+  }
+  ofstream ofconf(filename);
   if (!ofconf) {
     throw ApiException("Could not open config fragment file '" + filename + "' for writing: " + stringerror());
   }
@@ -89,26 +96,49 @@ static void apiServerConfigACL(const std::string& aclType, HttpRequest* req, Htt
       throw ApiException("'value' must be an array");
     }
 
-    NetmaskGroup nmg;
-    for (const auto& value : jlist.array_items()) {
+    if (g_yamlSettings) {
+      ::rust::Vec<::rust::String> vec;
+      for (const auto& value : jlist.array_items()) {
+        vec.emplace_back(value.string_value());
+      }
+
       try {
-        nmg.addMask(value.string_value());
+        ::pdns::rust::settings::rec::validate_allow_from(aclType, vec);
       }
-      catch (const NetmaskException& e) {
-        throw ApiException(e.reason);
+      catch (const ::rust::Error& e) {
+        throw ApiException(string("Unable to convert: ") + e.what());
       }
+      ::rust::String yaml;
+      if (aclType == "allow-from") {
+        yaml = pdns::rust::settings::rec::allow_from_to_yaml_string_incoming("allow_from", "allow_from_file", vec);
+      }
+      else {
+        yaml = pdns::rust::settings::rec::allow_from_to_yaml_string_incoming("allow_notify_from", "allow_notify_from_file", vec);
+      }
+      apiWriteConfigFile(aclType, string(yaml));
     }
+    else {
+      NetmaskGroup nmg;
+      for (const auto& value : jlist.array_items()) {
+        try {
+          nmg.addMask(value.string_value());
+        }
+        catch (const NetmaskException& e) {
+          throw ApiException(e.reason);
+        }
+      }
 
-    ostringstream strStream;
+      ostringstream strStream;
 
-    // Clear <foo>-from-file if set, so our changes take effect
-    strStream << aclType << "-file=" << endl;
+      // Clear <foo>-from-file if set, so our changes take effect
+      strStream << aclType << "-file=" << endl;
 
-    // Clear ACL setting, and provide a "parent" value
-    strStream << aclType << "=" << endl;
-    strStream << aclType << "+=" << nmg.toString() << endl;
+      // Clear ACL setting, and provide a "parent" value
+      strStream << aclType << "=" << endl;
+      strStream << aclType << "+=" << nmg.toString() << endl;
 
-    apiWriteConfigFile(aclType, strStream.str());
+      apiWriteConfigFile(aclType, strStream.str());
+    }
 
     parseACLs();
 
@@ -195,6 +225,8 @@ static void doCreateZone(const Json& document)
   bool rdFlag = boolFromJson(document, "recursion_desired");
   string confbasename = "zone-" + apiZoneNameToId(zone);
 
+  const string yamlAPiZonesFile = ::arg()["api-config-dir"] + "/apizones";
+
   if (kind == "NATIVE") {
     if (rdFlag) {
       throw ApiException("kind=Native and recursion_desired are mutually exclusive");
@@ -224,35 +256,55 @@ static void doCreateZone(const Json& document)
     }
     ofzone.close();
 
-    apiWriteConfigFile(confbasename, "auth-zones+=" + zonename + "=" + zonefilename);
+    if (g_yamlSettings) {
+      pdns::rust::settings::rec::AuthZone authzone;
+      authzone.zone = zonename;
+      authzone.file = zonefilename;
+      pdns::rust::settings::rec::api_add_auth_zone(yamlAPiZonesFile, authzone);
+    }
+    else {
+      apiWriteConfigFile(confbasename, "auth-zones+=" + zonename + "=" + zonefilename);
+    }
   }
   else if (kind == "FORWARDED") {
-    string serverlist;
-    for (const auto& value : document["servers"].array_items()) {
-      const string& server = value.string_value();
-      if (server.empty()) {
-        throw ApiException("Forwarded-to server must not be an empty string");
+    if (g_yamlSettings) {
+      pdns::rust::settings::rec::ForwardZone forward;
+      forward.zone = zonename;
+      forward.recurse = rdFlag;
+      forward.notify_allowed = false;
+      for (const auto& value : document["servers"].array_items()) {
+        forward.forwarders.emplace_back(value.string_value());
       }
-      try {
-        ComboAddress address = parseIPAndPort(server, 53);
-        if (!serverlist.empty()) {
-          serverlist += ";";
+      pdns::rust::settings::rec::api_add_forward_zone(yamlAPiZonesFile, forward);
+    }
+    else {
+      string serverlist;
+      for (const auto& value : document["servers"].array_items()) {
+        const string& server = value.string_value();
+        if (server.empty()) {
+          throw ApiException("Forwarded-to server must not be an empty string");
+        }
+        try {
+          ComboAddress address = parseIPAndPort(server, 53);
+          if (!serverlist.empty()) {
+            serverlist += ";";
+          }
+          serverlist += address.toStringWithPort();
+        }
+        catch (const PDNSException& e) {
+          throw ApiException(e.reason);
         }
-        serverlist += address.toStringWithPort();
       }
-      catch (const PDNSException& e) {
-        throw ApiException(e.reason);
+      if (serverlist.empty()) {
+        throw ApiException("Need at least one upstream server when forwarding");
       }
-    }
-    if (serverlist.empty()) {
-      throw ApiException("Need at least one upstream server when forwarding");
-    }
 
-    if (rdFlag) {
-      apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename + "=" + serverlist);
-    }
-    else {
-      apiWriteConfigFile(confbasename, "forward-zones+=" + zonename + "=" + serverlist);
+      if (rdFlag) {
+        apiWriteConfigFile(confbasename, "forward-zones-recurse+=" + zonename + "=" + serverlist);
+      }
+      else {
+        apiWriteConfigFile(confbasename, "forward-zones+=" + zonename + "=" + serverlist);
+      }
     }
   }
   else {
@@ -267,13 +319,17 @@ static bool doDeleteZone(const DNSName& zonename)
   }
 
   string filename;
-
-  // this one must exist
-  filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf";
-  if (unlink(filename.c_str()) != 0) {
-    return false;
+  if (g_yamlSettings) {
+    const string yamlAPiZonesFile = ::arg()["api-config-dir"] + "/apizones";
+    pdns::rust::settings::rec::api_delete_zone(yamlAPiZonesFile, zonename.toString());
+  }
+  else {
+    // this one must exist
+    filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".conf";
+    if (unlink(filename.c_str()) != 0) {
+      return false;
+    }
   }
-
   // .zone file is optional
   filename = ::arg()["api-config-dir"] + "/zone-" + apiZoneNameToId(zonename) + ".zone";
   unlink(filename.c_str());
@@ -298,7 +354,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp)
     }
 
     doCreateZone(document);
-    reloadZoneConfiguration();
+    reloadZoneConfiguration(g_yamlSettings);
     fillZone(zonename, resp);
     resp->status = 201;
     return;
@@ -342,7 +398,7 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
 
     doDeleteZone(zonename);
     doCreateZone(document);
-    reloadZoneConfiguration();
+    reloadZoneConfiguration(g_yamlSettings);
     resp->body = "";
     resp->status = 204; // No Content, but indicate success
   }
@@ -351,7 +407,7 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
       throw ApiException("Deleting domain failed");
     }
 
-    reloadZoneConfiguration();
+    reloadZoneConfiguration(g_yamlSettings);
     // empty body on success
     resp->body = "";
     resp->status = 204; // No Content: declare that the zone is gone now
index d443eca5d1aa585156624052fe2bc58072b21cc3..fcd61ea7b4ed74e67fac47fd1dadf784ddb698af 100644 (file)
@@ -12,3 +12,7 @@
 /recursor.conf
 /rec-conf.d
 /bindbackend.conf
+/acl-notify.list
+/acl-notify.list.yml
+/acl.list.yml
+/recursor.yml
index 515439d7bb5790209afd12255f96fddfe1aa4932..50065051d1acc1ed6e617b0b8a0988d6baea6103 100755 (executable)
@@ -84,32 +84,35 @@ BINDBACKEND_CONF_TPL = """
 ACL_LIST_TPL = """
 # Generated by runtests.py
 # local host
-127.0.0.1
-::1
+127.0.0.1
+::1
 """
 
 ACL_NOTIFY_LIST_TPL = """
 # Generated by runtests.py
 # local host
-127.0.0.1
-::1
+127.0.0.1
+::1
 """
 
 REC_EXAMPLE_COM_CONF_TPL = """
 # Generated by runtests.py
-auth-zones+=example.com=../regression-tests/zones/example.com.rec
+recursor:
+  auth_zones:
+  - zone: example.com
+    file: ../regression-tests/zones/example.com.rec
 """
 
 REC_CONF_TPL = """
 # Generated by runtests.py
-auth-zones=
-forward-zones=
-forward-zones-recurse=
-allow-from-file=acl.list
-allow-notify-from-file=acl-notify.list
-api-config-dir=%(conf_dir)s
-include-dir=%(conf_dir)s
-devonly-regression-test-mode
+incoming:
+  allow_from_file: acl.list.yml
+  allow_notify_from_file: acl-notify.list.yml
+webservice:
+  api_dir: %(conf_dir)s
+recursor:
+  include_dir: %(conf_dir)s
+  devonly_regression_test_mode: true
 """
 
 
@@ -222,13 +225,13 @@ if daemon == 'authoritative':
 else:
     conf_dir = 'rec-conf.d'
     ensure_empty_dir(conf_dir)
-    with open('acl.list', 'w') as acl_list:
+    with open('acl.list.yml', 'w') as acl_list:
         acl_list.write(ACL_LIST_TPL)
-    with open('acl-notify.list', 'w') as acl_notify_list:
+    with open('acl-notify.list.yml', 'w') as acl_notify_list:
         acl_notify_list.write(ACL_NOTIFY_LIST_TPL)
-    with open('recursor.conf', 'w') as recursor_conf:
+    with open('recursor.yml', 'w') as recursor_conf:
         recursor_conf.write(REC_CONF_TPL % locals())
-    with open(conf_dir+'/example.com..conf', 'w') as conf_file:
+    with open(conf_dir+'/example.com.yml', 'w') as conf_file:
         conf_file.write(REC_EXAMPLE_COM_CONF_TPL)
 
     servercmd = [pdns_recursor] + common_args
index cf883fb4ee1dfbfcbdd9b8e85fa3910853305d4e..c19426965e0b892733af3bd26912b31667576584 100644 (file)
@@ -58,6 +58,28 @@ loglevel=9
 disable-syslog=yes
 log-common-errors=yes
 statistics-interval=0
+"""
+    _config_template_yaml_default = """
+recursor:
+  daemon: false
+  threads: 2
+  include_dir: %s
+recordcache:
+  max_ttl: 15
+incoming:
+  listen:
+  - 127.0.0.1
+packetcache:
+  ttl: 15
+  servfail_ttl: 15
+outgoing:
+  dont_query: []
+logging:
+  trace: true
+  disable_syslog: true
+  common_errors: true
+  loglevel: 9
+  statistics_interval: 0
 """
     _config_template = """
 """
@@ -620,6 +642,44 @@ distributor-threads={threads}""".format(confdir=confdir,
                     roothints.write(cls._roothints)
                 conf.write("hint-file=%s\n" % roothintspath)
 
+    @classmethod
+    def generateRecursorYamlConfig(cls, confdir):
+        params = tuple([getattr(cls, param) for param in cls._config_params])
+        if len(params):
+            print(params)
+
+        recursorconf = os.path.join(confdir, 'recursor.yml')
+
+        with open(recursorconf, 'w') as conf:
+            conf.write("# Autogenerated by recursortests.py\n")
+            conf.write(cls._config_template_yaml_default % confdir)
+        recursorconf = os.path.join(confdir, 'recursor01.yml')
+        with open(recursorconf, 'w') as conf:
+            conf.write(cls._config_template % params)
+            conf.write("\n")
+        recursorconf = os.path.join(confdir, 'recursor02.yml')
+        with open(recursorconf, 'w') as conf:
+            conf.write("recursor:\n")
+            conf.write("  socket_dir: %s\n" % confdir)
+            if cls._lua_config_file or cls._root_DS:
+                luaconfpath = os.path.join(confdir, 'conffile.lua')
+                with open(luaconfpath, 'w') as luaconf:
+                    if cls._root_DS:
+                        luaconf.write("addTA('.', '%s')\n" % cls._root_DS)
+                    if cls._lua_config_file:
+                        luaconf.write(cls._lua_config_file)
+                conf.write("  lua_config_file: %s\n" % luaconfpath)
+            if cls._lua_dns_script_file:
+                luascriptpath = os.path.join(confdir, 'dnsscript.lua')
+                with open(luascriptpath, 'w') as luascript:
+                    luascript.write(cls._lua_dns_script_file)
+                conf.write("  lua_dns_script: %s\n" % luascriptpath)
+            if cls._roothints:
+                roothintspath = os.path.join(confdir, 'root.hints')
+                with open(roothintspath, 'w') as roothints:
+                    roothints.write(cls._roothints)
+                conf.write("  hint_file: %s\n" % roothintspath)
+
     @classmethod
     def startResponders(cls):
         pass
diff --git a/regression-tests.recursor-dnssec/test_SimpleYAML.py b/regression-tests.recursor-dnssec/test_SimpleYAML.py
new file mode 100644 (file)
index 0000000..22775ea
--- /dev/null
@@ -0,0 +1,112 @@
+import dns
+import os
+from recursortests import RecursorTest
+
+class testSimple(RecursorTest):
+    _confdir = 'Simple'
+
+    _config_template = """
+recursor:
+  auth_zones:
+  - zone: authzone.example
+    file: configs/%s/authzone.zone
+dnssec:
+  validation: validate""" % _confdir
+
+    @classmethod
+    def generateRecursorConfig(cls, confdir):
+        authzonepath = os.path.join(confdir, 'authzone.zone')
+        with open(authzonepath, 'w') as authzone:
+            authzone.write("""$ORIGIN authzone.example.
+@ 3600 IN SOA {soa}
+@ 3600 IN A 192.0.2.88
+""".format(soa=cls._SOA))
+        super(testSimple, cls).generateRecursorYamlConfig(confdir)
+
+    def testSOAs(self):
+        for zone in ['.', 'example.', 'secure.example.']:
+            expected = dns.rrset.from_text(zone, 0, dns.rdataclass.IN, 'SOA', self._SOA)
+            query = dns.message.make_query(zone, 'SOA', want_dnssec=True)
+            query.flags |= dns.flags.AD
+
+            res = self.sendUDPQuery(query)
+
+            self.assertMessageIsAuthenticated(res)
+            self.assertRRsetInAnswer(res, expected)
+            self.assertMatchingRRSIGInAnswer(res, expected)
+
+    def testA(self):
+        expected = dns.rrset.from_text('ns.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.9'.format(prefix=self._PREFIX))
+        query = dns.message.make_query('ns.secure.example', 'A', want_dnssec=True)
+        query.flags |= dns.flags.AD
+
+        res = self.sendUDPQuery(query)
+
+        self.assertMessageIsAuthenticated(res)
+        self.assertRRsetInAnswer(res, expected)
+        self.assertMatchingRRSIGInAnswer(res, expected)
+
+    def testDelegation(self):
+        query = dns.message.make_query('example', 'NS', want_dnssec=True)
+        query.flags |= dns.flags.AD
+
+        expectedNS = dns.rrset.from_text('example.', 0, 'IN', 'NS', 'ns1.example.', 'ns2.example.')
+
+        res = self.sendUDPQuery(query)
+
+        self.assertMessageIsAuthenticated(res)
+        self.assertRRsetInAnswer(res, expectedNS)
+
+    def testBogus(self):
+        query = dns.message.make_query('ted.bogus.example', 'A', want_dnssec=True)
+
+        res = self.sendUDPQuery(query)
+
+        self.assertRcodeEqual(res, dns.rcode.SERVFAIL)
+
+    def testAuthZone(self):
+        query = dns.message.make_query('authzone.example', 'A', want_dnssec=True)
+
+        expectedA = dns.rrset.from_text('authzone.example.', 0, 'IN', 'A', '192.0.2.88')
+
+        res = self.sendUDPQuery(query)
+
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expectedA)
+
+    def testLocalhost(self):
+        queryA = dns.message.make_query('localhost', 'A', want_dnssec=True)
+        expectedA = dns.rrset.from_text('localhost.', 0, 'IN', 'A', '127.0.0.1')
+
+        queryPTR = dns.message.make_query('1.0.0.127.in-addr.arpa', 'PTR', want_dnssec=True)
+        expectedPTR = dns.rrset.from_text('1.0.0.127.in-addr.arpa.', 0, 'IN', 'PTR', 'localhost.')
+
+        resA = self.sendUDPQuery(queryA)
+        resPTR = self.sendUDPQuery(queryPTR)
+
+        self.assertRcodeEqual(resA, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(resA, expectedA)
+
+        self.assertRcodeEqual(resPTR, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(resPTR, expectedPTR)
+
+    def testLocalhostSubdomain(self):
+        queryA = dns.message.make_query('foo.localhost', 'A', want_dnssec=True)
+        expectedA = dns.rrset.from_text('foo.localhost.', 0, 'IN', 'A', '127.0.0.1')
+
+        resA = self.sendUDPQuery(queryA)
+
+        self.assertRcodeEqual(resA, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(resA, expectedA)
+
+    def testIslandOfSecurity(self):
+        query = dns.message.make_query('cname-to-islandofsecurity.secure.example.', 'A', want_dnssec=True)
+
+        expectedCNAME = dns.rrset.from_text('cname-to-islandofsecurity.secure.example.', 0, 'IN', 'CNAME', 'node1.islandofsecurity.example.')
+        expectedA = dns.rrset.from_text('node1.islandofsecurity.example.', 0, 'IN', 'A', '192.0.2.20')
+
+        res = self.sendUDPQuery(query)
+
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expectedA)
+
index 6115bd4ed5b7a0fae5bdc2661c3a044ac87ee6c9..e84fc968defb0c17f8faf384efa4121f0018c55c 100644 (file)
--- a/tasks.py
+++ b/tasks.py
@@ -8,6 +8,7 @@ import time
 auth_backend_ip_addr = os.getenv('AUTH_BACKEND_IP_ADDR', '127.0.0.1')
 
 clang_version = os.getenv('CLANG_VERSION', '13')
+rust_version = 'rust-1.72.0-x86_64-unknown-linux-gnu'
 
 all_build_deps = [
     'ccache',
@@ -170,6 +171,10 @@ def install_clang_runtime(c):
     # this gives us the symbolizer, for symbols in asan/ubsan traces
     c.sudo(f'apt-get -y --no-install-recommends install clang-{clang_version}')
 
+@task
+def ci_install_rust(c, repo):
+    c.sudo(f'{repo}/builder-support/helpers/install_rust.sh {rust_version}')
+
 def install_libdecaf(c, product):
     c.run('git clone https://git.code.sf.net/p/ed448goldilocks/code /tmp/libdecaf')
     with c.cd('/tmp/libdecaf'):