---
-Checks: 'clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,bugprone-*,concurrency-*,modernize-*,performance-*,portability-*,readability-*,-modernize-use-trailing-return-type,-readability-magic-numbers,-cppcoreguidelines-avoid-magic-numbers,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-avoid-non-const-global-variables,-cppcoreguidelines-pro-type-vararg,-cppcoreguidelines-avoid-do-while'
+Checks: 'clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,bugprone-*,concurrency-*,modernize-*,performance-*,portability-*,readability-*,-modernize-use-trailing-return-type,-readability-magic-numbers,-cppcoreguidelines-avoid-magic-numbers,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-avoid-non-const-global-variables,-cppcoreguidelines-pro-type-vararg,-cppcoreguidelines-avoid-do-while,-cppcoreguidelines-avoid-const-or-ref-data-members'
WarningsAsErrors: ''
HeaderFilterRegex: ''
AnalyzeTemporaryDtors: false
The images are ready to run with limited functionality.
At container startup, the startup.py wrapper (from the dockerdata directory linked above) checks for `PDNS_RECURSOR_API_KEY` / `PDNS_AUTH_API_KEY` / `DNSDIST_API_KEY` environment variables for the product you are running.
-If such a variable is found, `/etc/powerdns-api.conf` or `/etc/dnsdist-api.conf` is written, enabling the webserver in all products, and the dnsdist console.
+If such a variable is found, `/etc/powerdns/recursor.d/_api.conf` / `/etc/powerdns/pdns.d/_api.conf` / `/etc/dnsdist/conf.d/_api.conf` is written, enabling the webserver in all products, and the dnsdist console.
For the dnsdist console, make sure that your API key is in a format suitable for the console (use `makeKey()`).
The default configs shipped in the image (see dockerdata above) parse all files in `/etc/powerdns/pdns.d` / `/etc/powerdns/recursor.d` / `/etc/dnsdist/conf.d`.
-The image also ships a symlink to the API config file inside those `.d` dirs.
For Auth and Recursor, extra configuration can be passed on the command line, or via a volume mount into `/etc/powerdns` or the `.d` dir.
For dnsdist, only the volume mount is applicable.
-If you want to volume mount a config, but also take the keys from the environment, please take care to include the same `X-api.conf` symlink in your `.d` directory.
+If you want to volume mount a config, but also take the keys from the environment, please take care to include the same `_api.conf` file in your `.d` directory.
+
+If you want to read the configuration for debugging purposes, you can run the containers with the `DEBUG_CONFIG` environment variable set to `'yes'`.
+This will print the full config on startup. Please keep in mind that this also includes credentials, therefore this setting should never be used in production environments.
# Auth and databases
```
In the above example `/path/to/supervisord.conf` is the path where a configmap containing your supervisord configuration is mounted.
-Further details about `supervisord` and how to configure it can be found here: http://supervisord.org/configuration.html
\ No newline at end of file
+Further details about `supervisord` and how to configure it can be found here: http://supervisord.org/configuration.html
templateroot = '/etc/dnsdist/templates.d'
templatedestination = '/etc/dnsdist/conf.d'
+debug = os.getenv("DEBUG_CONFIG", 'no').lower() == 'yes'
+
apikey = os.getenv(apienvvar)
if apikey is not None:
webserver_conf = jinja2.Template(apiconftemplate).render(apikey=apikey)
conffile = os.path.join(templatedestination, '_api.conf')
with open(conffile, 'w') as f:
f.write(webserver_conf)
- print("Created {} with content:\n{}\n".format(conffile, webserver_conf))
+ if debug:
+ print("Created {} with content:\n{}\n".format(conffile, webserver_conf))
templates = os.getenv('TEMPLATE_FILES')
if templates is not None:
target = os.path.join(templatedestination, templateFile + '.conf')
with open(target, 'w') as f:
f.write(rendered)
- print("Created {} with content:\n{}\n".format(target, rendered))
+ if debug:
+ print("Created {} with content:\n{}\n".format(target, rendered))
os.execv(program, [program]+args+sys.argv[1:])
overload-drops
^^^^^^^^^^^^^^
-Number of questions dropped because backends overloaded
+Number of questions dropped because backends overloaded (backends are overloaded if they have more outstanding queries than the value of :ref:`setting-overload-queue-length`)
.. _stat-packetcache-hit:
-@ 86400 IN SOA pdns-public-ns1.powerdns.com. peter\.van\.dijk.powerdns.com. 2024021601 10800 3600 604800 10800
+@ 86400 IN SOA pdns-public-ns1.powerdns.com. peter\.van\.dijk.powerdns.com. 2024030700 10800 3600 604800 10800
@ 3600 IN NS pdns-public-ns1.powerdns.com.
@ 3600 IN NS pdns-public-ns2.powerdns.com.
recursor-4.8.4.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/recursor/security-advisories/powerdns-advisory-2024-01.html"
recursor-4.8.5.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/recursor/security-advisories/powerdns-advisory-2024-01.html"
recursor-4.8.6.security-status 60 IN TXT "1 OK"
+recursor-4.8.7.security-status 60 IN TXT "1 OK"
recursor-4.9.0-alpha1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-4.9.0-beta1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-4.9.0-rc1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-4.9.1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/recursor/security-advisories/powerdns-advisory-2024-01.html"
recursor-4.9.2.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/recursor/security-advisories/powerdns-advisory-2024-01.html"
recursor-4.9.3.security-status 60 IN TXT "1 OK"
+recursor-4.9.4.security-status 60 IN TXT "1 OK"
recursor-5.0.0-alpha1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-5.0.0-alpha2.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-5.0.0-beta1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-5.0.0.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)"
recursor-5.0.1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/recursor/security-advisories/powerdns-advisory-2024-01.html"
recursor-5.0.2.security-status 60 IN TXT "1 OK"
+recursor-5.0.3.security-status 60 IN TXT "1 OK"
; Recursor Debian
recursor-3.6.2-2.debian.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/3/security/powerdns-advisory-2015-01/ and https://doc.powerdns.com/3/security/powerdns-advisory-2016-02/"
- Default: 0 (disabled)
If this many packets are waiting for database attention, answer any new
-questions strictly from the packet cache.
+questions strictly from the packet cache. Packets not in the cache will
+be dropped, and :ref:`_stat-overload-drops` will be incremented.
.. _setting-prevent-self-notification:
DNSName after;
BOOST_TEST_MESSAGE("Testing getBeforeAndAfterNamesAbsolute method");
- backendUnderTest->getBeforeAndAfterNamesAbsolute(-1, DNSName("middle.unit.test."), unhashed, before, after);
+ backendUnderTest->getBeforeAndAfterNamesAbsolute(1, DNSName("middle.unit.test."), unhashed, before, after);
BOOST_CHECK_EQUAL(unhashed.toString(), "middle.");
BOOST_CHECK_EQUAL(before.toString(), "begin.");
BOOST_CHECK_EQUAL(after.toString(), "stop.");
;;
remotebackend_unix.test)
start_unix
- run_test
+ run_test ; rv=$?
stop_unix
;;
remotebackend_http.test)
start_web "http"
- run_test
+ run_test ; rv=$?
stop_web "http"
;;
remotebackend_post.test)
start_web "post"
- run_test
+ run_test ; rv=$?
stop_web "post"
;;
remotebackend_json.test)
start_web "json"
- run_test
+ run_test ; rv=$?
stop_web "json"
;;
remotebackend_zeromq.test)
start_zeromq
- run_test
+ run_test ; rv=$?
stop_zeromq
;;
*)
;;
esac
-exit $?
+exit $rv
[do_getdomaininfo({'name'=>'unit.test.'})]
end
- def do_getupdatedmasters()
+ def do_getupdatedmasters(args)
[do_getdomaininfo({'name'=>'master.test.'})]
end
end
if h.respond_to?(method.to_sym) == false
res = false
- elsif args.size > 0
- res, log = h.send(method,args)
else
- res, log = h.send(method)
+ res, log = h.send(method,args)
end
puts ({:result => res, :log => log}).to_json
f.puts "#{Time.now.to_f} [pipe]: #{({:result => res, :log => log}).to_json}"
if h.respond_to?(method.to_sym) == false
res = false
- elsif args.size > 0
- res, log = h.send(method,args)
else
- res, log = h.send(method)
+ res, log = h.send(method,args)
end
socket.send_string ({:result => res, :log => log}).to_json + "\n" , 0
f.puts "#{Time.now.to_f} [zmq]: #{({:result => res, :log => log}).to_json}"
UnixsocketConnector::~UnixsocketConnector()
{
if (this->connected) {
- try {
- g_log << Logger::Info << "closing socket connection" << endl;
- }
- catch (...) {
- }
close(fd);
}
}
utility.hh \
uuid-utils.hh uuid-utils.cc \
version.cc version.hh \
+ views.hh \
webserver.cc webserver.hh \
ws-api.cc ws-api.hh \
ws-auth.cc ws-auth.hh \
tcpiohandler.cc tcpiohandler.hh \
threadname.hh threadname.cc \
uuid-utils.hh uuid-utils.cc \
+ views.hh \
xpf.cc xpf.hh \
xsk.cc xsk.hh
{
using MaintenanceCallback = std::function<void()>;
void runMaintenanceHooks(const LuaContext& context);
-void addMaintenanceCallback(MaintenanceCallback callback);
+void addMaintenanceCallback(const LuaContext& context, MaintenanceCallback callback);
void clearMaintenanceHooks();
void setupLuaHooks(LuaContext& luaCtx);
}
(see https://github.com/h2o/h2o/issues/3230). See the ``library`` parameter on the :func:`addDOHLocal` directive for more information on how to select
the library used when dnsdist is built with support for both ``h2o`` and ``nghttp2``. The default is now ``nghttp2`` whenever possible.
Note that ``nghttp2`` only supports HTTP/2, and not HTTP/1, while ``h2o`` supported both. This is not an issue for actual DNS over HTTPS clients that
-support HTTP/2, but might be one in setups running dnsdist behind a reverse-proxy that does not support HTTP/1. See :doc:`guides/dns-over-https` for some work-around.
+support HTTP/2, but might be one in setups running dnsdist behind a reverse-proxy that does not support HTTP/2. See :doc:`guides/dns-over-https` for some work-around.
SNMP support is no longer enabled by default during ``configure``, requiring ``--with-net-snmp`` to be built.
--- /dev/null
+../views.hh
\ No newline at end of file
}
// this parses a DNS name until a compression pointer is found
-size_t DNSName::parsePacketUncompressed(const UnsignedCharView& view, size_t pos, bool uncompress)
+size_t DNSName::parsePacketUncompressed(const pdns::views::UnsignedCharView& view, size_t pos, bool uncompress)
{
const size_t initialPos = pos;
size_t totalLength = 0;
}
unsigned char labellen{0};
- UnsignedCharView view(qpos, len);
+ pdns::views::UnsignedCharView view(qpos, len);
auto pos = parsePacketUncompressed(view, offset, uncompress);
labellen = view.at(pos);
}
#include "burtle.hh"
+#include "views.hh"
// #include "dns.hh"
// #include "logger.hh"
private:
string_t d_storage;
- class UnsignedCharView
- {
- public:
- UnsignedCharView(const char* data_, size_t size_): view(data_, size_)
- {
- }
- const unsigned char& at(std::string_view::size_type pos) const
- {
- return reinterpret_cast<const unsigned char&>(view.at(pos));
- }
-
- size_t size() const
- {
- return view.size();
- }
-
- private:
- std::string_view view;
- };
-
void packetParser(const char* qpos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, int depth, uint16_t minOffset);
- size_t parsePacketUncompressed(const UnsignedCharView& view, size_t position, bool uncompress);
+ size_t parsePacketUncompressed(const pdns::views::UnsignedCharView& view, size_t position, bool uncompress);
static void appendEscapedLabel(std::string& appendTo, const char* orig, size_t len);
static std::string unescapeLabel(const std::string& orig);
static void throwSafeRangeError(const std::string& msg, const char* buf, size_t length);
#include "dolog.hh"
#endif
-#define DNSTAP_CONTENT_TYPE "protobuf:dnstap.Dnstap"
-
#ifdef HAVE_FSTRM
-FrameStreamLogger::FrameStreamLogger(const int family, const std::string& address, bool connect,
- const std::unordered_map<string,unsigned>& options): d_family(family), d_address(address)
-{
- fstrm_res res;
+static const std::string DNSTAP_CONTENT_TYPE = "protobuf:dnstap.Dnstap";
+FrameStreamLogger::FrameStreamLogger(const int family, std::string address, bool connect, const std::unordered_map<string, unsigned>& options) :
+ d_family(family), d_address(std::move(address))
+{
try {
d_fwopt = fstrm_writer_options_init();
- if (!d_fwopt) {
+ if (d_fwopt == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_writer_options_init failed.");
}
- res = fstrm_writer_options_add_content_type(d_fwopt, DNSTAP_CONTENT_TYPE, sizeof(DNSTAP_CONTENT_TYPE) - 1);
+ auto res = fstrm_writer_options_add_content_type(d_fwopt, DNSTAP_CONTENT_TYPE.c_str(), DNSTAP_CONTENT_TYPE.size());
if (res != fstrm_res_success) {
throw std::runtime_error("FrameStreamLogger: fstrm_writer_options_add_content_type failed: " + std::to_string(res));
}
if (d_family == AF_UNIX) {
- struct sockaddr_un local;
- if (makeUNsockaddr(d_address, &local)) {
+ struct sockaddr_un local
+ {
+ };
+ if (makeUNsockaddr(d_address, &local) != 0) {
throw std::runtime_error("FrameStreamLogger: Unable to use '" + d_address + "', it is not a valid UNIX socket path.");
}
d_uwopt = fstrm_unix_writer_options_init();
- if (!d_uwopt) {
+ if (d_uwopt == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_unix_writer_options_init failed.");
}
fstrm_unix_writer_options_set_socket_path(d_uwopt, d_address.c_str());
d_writer = fstrm_unix_writer_init(d_uwopt, d_fwopt);
- if (!d_writer) {
+ if (d_writer == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_unix_writer_init() failed.");
}
- #ifdef HAVE_FSTRM_TCP_WRITER_INIT
- } else if (family == AF_INET) {
+#ifdef HAVE_FSTRM_TCP_WRITER_INIT
+ }
+ else if (family == AF_INET || family == AF_INET6) {
d_twopt = fstrm_tcp_writer_options_init();
- if (!d_twopt) {
+ if (d_twopt == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_tcp_writer_options_init failed.");
}
try {
- ComboAddress ca(d_address);
+ ComboAddress inetAddress(d_address);
// void return, no error checking.
- fstrm_tcp_writer_options_set_socket_address(d_twopt, ca.toString().c_str());
- fstrm_tcp_writer_options_set_socket_port(d_twopt, std::to_string(ca.getPort()).c_str());
- } catch (PDNSException &e) {
+ fstrm_tcp_writer_options_set_socket_address(d_twopt, inetAddress.toString().c_str());
+ fstrm_tcp_writer_options_set_socket_port(d_twopt, std::to_string(inetAddress.getPort()).c_str());
+ }
+ catch (PDNSException& e) {
throw std::runtime_error("FrameStreamLogger: Unable to use '" + d_address + "': " + e.reason);
}
d_writer = fstrm_tcp_writer_init(d_twopt, d_fwopt);
- if (!d_writer) {
+ if (d_writer == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_tcp_writer_init() failed.");
}
- #endif
- } else {
+#endif
+ }
+ else {
throw std::runtime_error("FrameStreamLogger: family " + std::to_string(family) + " not supported");
}
d_iothropt = fstrm_iothr_options_init();
- if (!d_iothropt) {
+ if (d_iothropt == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_iothr_options_init() failed.");
}
throw std::runtime_error("FrameStreamLogger: fstrm_iothr_options_set_queue_model failed: " + std::to_string(res));
}
- const struct {
+ struct setters
+ {
const std::string name;
- fstrm_res (*function)(struct fstrm_iothr_options *, const unsigned int);
- } list[] = {
- { "bufferHint", fstrm_iothr_options_set_buffer_hint },
- { "flushTimeout", fstrm_iothr_options_set_flush_timeout },
- { "inputQueueSize", fstrm_iothr_options_set_input_queue_size },
- { "outputQueueSize", fstrm_iothr_options_set_output_queue_size },
- { "queueNotifyThreshold", fstrm_iothr_options_set_queue_notify_threshold },
- { "setReopenInterval", fstrm_iothr_options_set_reopen_interval }
+ fstrm_res (*function)(struct fstrm_iothr_options*, const unsigned int);
};
-
- for (const auto& i : list) {
- if (options.find(i.name) != options.end() && options.at(i.name)) {
- fstrm_res r = i.function(d_iothropt, options.at(i.name));
- if (r != fstrm_res_success) {
- throw std::runtime_error("FrameStreamLogger: setting " + string(i.name) + " failed: " + std::to_string(r));
+ const std::array<struct setters, 6> list = {{{"bufferHint", fstrm_iothr_options_set_buffer_hint},
+ {"flushTimeout", fstrm_iothr_options_set_flush_timeout},
+ {"inputQueueSize", fstrm_iothr_options_set_input_queue_size},
+ {"outputQueueSize", fstrm_iothr_options_set_output_queue_size},
+ {"queueNotifyThreshold", fstrm_iothr_options_set_queue_notify_threshold},
+ {"setReopenInterval", fstrm_iothr_options_set_reopen_interval}}};
+
+ for (const auto& entry : list) {
+ if (auto option = options.find(entry.name); option != options.end() && option->second != 0) {
+ auto result = entry.function(d_iothropt, option->second);
+ if (result != fstrm_res_success) {
+ throw std::runtime_error("FrameStreamLogger: setting " + string(entry.name) + " failed: " + std::to_string(result));
}
}
}
if (connect) {
d_iothr = fstrm_iothr_init(d_iothropt, &d_writer);
- if (!d_iothr) {
+ if (d_iothr == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_iothr_init() failed.");
}
d_ioqueue = fstrm_iothr_get_input_queue(d_iothr);
- if (!d_ioqueue) {
+ if (d_ioqueue == nullptr) {
throw std::runtime_error("FrameStreamLogger: fstrm_iothr_get_input_queue() failed.");
}
}
- } catch (std::runtime_error &e) {
+ }
+ catch (std::runtime_error& e) {
this->cleanup();
throw;
}
RemoteLoggerInterface::Result FrameStreamLogger::queueData(const std::string& data)
{
- if (!d_ioqueue || !d_iothr) {
+ if ((d_ioqueue == nullptr) || d_iothr == nullptr) {
++d_permanentFailures;
return Result::OtherError;
}
- uint8_t *frame = (uint8_t*)malloc(data.length());
- if (!frame) {
+ uint8_t* frame = (uint8_t*)malloc(data.length()); // NOLINT: it's the API
+ if (frame == nullptr) {
++d_queueFullDrops; // XXX separate count?
return Result::TooLarge;
}
memcpy(frame, data.c_str(), data.length());
- fstrm_res res;
- res = fstrm_iothr_submit(d_iothr, d_ioqueue, frame, data.length(), fstrm_free_wrapper, nullptr);
+ auto res = fstrm_iothr_submit(d_iothr, d_ioqueue, frame, data.length(), fstrm_free_wrapper, nullptr);
if (res == fstrm_res_success) {
// Frame successfully queued.
++d_framesSent;
+ // do not call free here
return Result::Queued;
- } else if (res == fstrm_res_again) {
- free(frame);
+ }
+ if (res == fstrm_res_again) {
+ free(frame); // NOLINT: it's the API
++d_queueFullDrops;
return Result::PipeFull;
- } else {
- // Permanent failure.
- free(frame);
- ++d_permanentFailures;
- return Result::OtherError;
}
+ // Permanent failure.
+ free(frame); // NOLINT: it's the API
+ ++d_permanentFailures;
+ return Result::OtherError;
}
#endif /* HAVE_FSTRM */
#include <fstrm/tcp_writer.h>
#endif
-class FrameStreamLogger : public RemoteLoggerInterface, boost::noncopyable
+class FrameStreamLogger : public RemoteLoggerInterface
{
public:
- FrameStreamLogger(int family, const std::string& address, bool connect, const std::unordered_map<string,unsigned>& options = std::unordered_map<string,unsigned>());
- ~FrameStreamLogger();
+ FrameStreamLogger(int family, std::string address, bool connect, const std::unordered_map<string, unsigned>& options = std::unordered_map<string, unsigned>());
+ FrameStreamLogger(const FrameStreamLogger&) = delete;
+ FrameStreamLogger(FrameStreamLogger&&) = delete;
+ FrameStreamLogger& operator=(const FrameStreamLogger&) = delete;
+ FrameStreamLogger& operator=(FrameStreamLogger&&) = delete;
+ ~FrameStreamLogger() override;
[[nodiscard]] RemoteLoggerInterface::Result queueData(const std::string& data) override;
[[nodiscard]] std::string address() const override
{
return "dnstap";
}
-
+
[[nodiscard]] std::string toString() override
{
return "FrameStreamLogger to " + d_address + " (" + std::to_string(d_framesSent) + " frames sent, " + std::to_string(d_queueFullDrops) + " dropped, " + std::to_string(d_permanentFailures) + " permanent failures)";
return Stats{.d_queued = d_framesSent,
.d_pipeFull = d_queueFullDrops,
.d_tooLarge = 0,
- .d_otherError = d_permanentFailures
- };
+ .d_otherError = d_permanentFailures};
}
private:
-
const int d_family;
const std::string d_address;
- struct fstrm_iothr_queue *d_ioqueue{nullptr};
- struct fstrm_writer_options *d_fwopt{nullptr};
- struct fstrm_unix_writer_options *d_uwopt{nullptr};
+ struct fstrm_iothr_queue* d_ioqueue{nullptr};
+ struct fstrm_writer_options* d_fwopt{nullptr};
+ struct fstrm_unix_writer_options* d_uwopt{nullptr};
#ifdef HAVE_FSTRM_TCP_WRITER_INIT
- struct fstrm_tcp_writer_options *d_twopt{nullptr};
+ struct fstrm_tcp_writer_options* d_twopt{nullptr};
#endif
- struct fstrm_writer *d_writer{nullptr};
- struct fstrm_iothr_options *d_iothropt{nullptr};
- struct fstrm_iothr *d_iothr{nullptr};
+ struct fstrm_writer* d_writer{nullptr};
+ struct fstrm_iothr_options* d_iothropt{nullptr};
+ struct fstrm_iothr* d_iothr{nullptr};
std::atomic<uint64_t> d_framesSent{0};
std::atomic<uint64_t> d_queueFullDrops{0};
std::atomic<uint64_t> d_permanentFailures{0};
};
#else
-class FrameStreamLogger : public RemoteLoggerInterface, boost::noncopyable {};
+class FrameStreamLogger : public RemoteLoggerInterface
+{
+ FrameStreamLogger(const FrameStreamLogger&) = delete;
+ FrameStreamLogger(FrameStreamLogger&&) = delete;
+ FrameStreamLogger& operator=(const FrameStreamLogger&) = delete;
+ FrameStreamLogger& operator=(FrameStreamLogger&&) = delete;
+};
#endif /* HAVE_FSTRM */
uuid-utils.hh uuid-utils.cc \
validate.cc validate.hh validate-recursor.cc validate-recursor.hh \
version.cc version.hh \
+ views.hh \
webserver.cc webserver.hh \
ws-api.cc ws-api.hh \
ws-recursor.cc ws-recursor.hh \
Changelogs for 4.8.X
====================
+.. changelog::
+ :version: 4.8.7
+ :released: 7th of March 2024
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13797
+ :tickets: 13353
+
+ If serving stale, wipe CNAME records from cache when we get a NODATA negative response for them.
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13799
+
+ Fix the zoneToCache regression introduced by SA 2024-01.
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13854
+ :tickets: 13847
+
+ Fix gathering of denial of existence proof for wildcard-expanded names.
+
+ .. change::
+ :tags: Improvements
+ :pullreq: 13796
+ :tickets: 13387
+
+ Update new b-root-server.net addresses in built-in hints.
.. changelog::
:version: 4.8.6
Changelogs for 4.9.X
====================
+.. changelog::
+ :version: 4.9.4
+ :released: 7th of March 2024
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13853
+
+ Fix gathering of denial of existence proof for wildcard-expanded names.
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13795
+ :tickets: 13788
+
+ Fix the zoneToCache regression introduced by SA 2024-01.
+
+ .. change::
+ :tags: Improvements
+ :pullreq: 13793
+ :tickets: 13387, 12897
+
+ Update new b-root-server.net addresses in built-in hints.
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13792
+ :tickets: 13543
+
+ A single NSEC3 record covering everything is a special case.
+
.. changelog::
:version: 4.9.3
:released: 13th of February 2024
Before upgrading, it is advised to read the :doc:`../upgrade`.
+.. changelog::
+ :version: 5.0.3
+ :released: 7th of March 2024
+
+ .. change::
+ :tags: Improvements
+ :pullreq: 13845
+ :tickets: 13824
+
+ Log if a DNSSEC related limit was hit if log_bogus is set.
+
+ .. change::
+ :tags: Improvements
+ :pullreq: 13846
+ :tickets: 13830
+
+ Reduce RPZ memory usage by not keeping the initially loaded RPZs in memory.
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13852
+ :tickets: 13847
+
+ Fix gathering of denial of existence proof for wildcard-expanded names.
+
+ .. change::
+ :tags: Bug Fixes
+ :pullreq: 13791
+ :tickets: 13788
+
+ Fix the zoneToCache regression introduced by SA 2024-01.
+
.. changelog::
:version: 5.0.2
:released: 13th of February 2024
We would like to thank Elias Heftrig, Haya Schulmann, Niklas Vogel, and Michael Waidner from the
German National Research Center for Applied Cybersecurity ATHENE for bringing this issue to the
attention of the DNS community and especially Niklas Vogel for his assistance in validating the
-patches.
+patches. We would also like to thank Petr Špaček from ISC for discovering and responsibly disclosing
+CVE-2023-50868.
Before upgrading, it is advised to read the :doc:`changelog/index`.
When upgrading several versions, please read **all** notes applying to the upgrade.
-5.0.1 to 5.0.2 and master, 4.9.2 to 4.9.3 and 4.8.5 to 4.8.6
+5.0.2 to 5.0.3 and master, 4.9.3 to 4.9.4 and 4.8.6 to 4.8.7
+------------------------------------------------------------
+
+Known Issue Solved
+^^^^^^^^^^^^^^^^^^
+The DNSSEC validation issue with the :func:`zoneToCache` function has been resolved and workarounds can be removed.
+
+5.0.1 to 5.0.2, 4.9.2 to 4.9.3 and 4.8.5 to 4.8.6
------------------------------------------------------------
Known Issues
bool DNSFilterEngine::Zone::findExactNSPolicy(const DNSName& qname, DNSFilterEngine::Policy& pol) const
{
if (findExactNamedPolicy(d_propolName, qname, pol)) {
- pol.d_trigger = qname;
- pol.d_trigger.appendRawLabel(rpzNSDnameName);
+ // hitdata set by findExactNamedPolicy
+ pol.d_hitdata->d_trigger.appendRawLabel(rpzNSDnameName);
return true;
}
return false;
{
if (const auto* fnd = d_propolNSAddr.lookup(addr)) {
pol = fnd->second;
- pol.d_trigger = Zone::maskToRPZ(fnd->first);
- pol.d_trigger.appendRawLabel(rpzNSIPName);
- pol.d_hit = addr.toString();
+ pol.setHitData(Zone::maskToRPZ(fnd->first), addr.toString());
+ pol.d_hitdata->d_trigger.appendRawLabel(rpzNSIPName);
return true;
}
return false;
{
if (const auto* fnd = d_postpolAddr.lookup(addr)) {
pol = fnd->second;
- pol.d_trigger = Zone::maskToRPZ(fnd->first);
- pol.d_trigger.appendRawLabel(rpzIPName);
- pol.d_hit = addr.toString();
+ pol.setHitData(Zone::maskToRPZ(fnd->first), addr.toString());
+ pol.d_hitdata->d_trigger.appendRawLabel(rpzIPName);
return true;
}
return false;
{
if (const auto* fnd = d_qpolAddr.lookup(addr)) {
pol = fnd->second;
- pol.d_trigger = Zone::maskToRPZ(fnd->first);
- pol.d_trigger.appendRawLabel(rpzClientIPName);
- pol.d_hit = addr.toString();
+ pol.setHitData(Zone::maskToRPZ(fnd->first), addr.toString());
+ pol.d_hitdata->d_trigger.appendRawLabel(rpzClientIPName);
return true;
}
return false;
iter = polmap.find(g_wildcarddnsname + sub);
if (iter != polmap.end()) {
pol = iter->second;
- pol.d_trigger = iter->first;
- pol.d_hit = qname.toStringNoDot();
+ pol.setHitData(iter->first, qname.toStringNoDot());
return true;
}
}
const auto iter = polmap.find(qname);
if (iter != polmap.end()) {
pol = iter->second;
- pol.d_trigger = qname;
- pol.d_hit = qname.toStringNoDot();
+ pol.setHitData(qname, qname.toStringNoDot());
return true;
}
if (zone->findExactNSPolicy(wildcard, pol)) {
// cerr<<"Had a hit on the nameserver ("<<qname<<") used to process the query"<<endl;
// Hit is not the wildcard passed to findExactQNamePolicy but the actual qname!
- pol.d_hit = qname.toStringNoDot();
+ pol.d_hitdata->d_hit = qname.toStringNoDot();
return true;
}
}
if (zone->findExactQNamePolicy(wildcard, pol)) {
// cerr<<"Had a hit on the name of the query"<<endl;
// Hit is not the wildcard passed to findExactQNamePolicy but the actual qname!
- pol.d_hit = qname.toStringNoDot();
+ pol.d_hitdata->d_hit = qname.toStringNoDot();
return true;
}
}
}
}
+static void addCustom(DNSFilterEngine::Policy& existingPol, const DNSFilterEngine::Policy& pol)
+{
+ if (!existingPol.d_custom) {
+ existingPol.d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
+ }
+ if (pol.d_custom) {
+ existingPol.d_custom->reserve(existingPol.d_custom->size() + pol.d_custom->size());
+ std::move(pol.d_custom->begin(), pol.d_custom->end(), std::back_inserter(*existingPol.d_custom));
+ }
+}
+
void DNSFilterEngine::Zone::addNameTrigger(std::unordered_map<DNSName, Policy>& map, const DNSName& n, Policy&& pol, bool ignoreDuplicate, PolicyType ptype)
{
auto iter = map.find(n);
throw std::runtime_error("Adding a " + getTypeToString(ptype) + "-based filter policy of kind " + getKindToString(pol.d_kind) + " but a policy of kind " + getKindToString(existingPol.d_kind) + " already exists for for the following name: " + n.toLogString());
}
- existingPol.d_custom.reserve(existingPol.d_custom.size() + pol.d_custom.size());
-
- std::move(pol.d_custom.begin(), pol.d_custom.end(), std::back_inserter(existingPol.d_custom));
+ addCustom(existingPol, pol);
}
else {
auto& qpol = map.insert({n, std::move(pol)}).first->second;
throw std::runtime_error("Adding a " + getTypeToString(ptype) + "-based filter policy of kind " + getKindToString(pol.d_kind) + " but a policy of kind " + getKindToString(existingPol.d_kind) + " already exists for the following netmask: " + netmask.toString());
}
- existingPol.d_custom.reserve(existingPol.d_custom.size() + pol.d_custom.size());
-
- std::move(pol.d_custom.begin(), pol.d_custom.end(), std::back_inserter(existingPol.d_custom));
+ addCustom(existingPol, pol);
}
else {
pol.d_zoneData = d_zoneData;
/* for custom types, we might have more than one type,
and then we need to remove only the right ones. */
bool result = false;
- for (const auto& toRemove : pol.d_custom) {
- for (auto it = existing.d_custom.begin(); it != existing.d_custom.end(); ++it) {
- if (**it == *toRemove) {
- existing.d_custom.erase(it);
- result = true;
- break;
+ if (pol.d_custom && existing.d_custom) {
+ for (const auto& toRemove : *pol.d_custom) {
+ for (auto it = existing.d_custom->begin(); it != existing.d_custom->end(); ++it) {
+ if (**it == *toRemove) {
+ existing.d_custom->erase(it);
+ result = true;
+ break;
+ }
}
}
}
// No records left for this trigger?
- if (existing.d_custom.empty()) {
+ if (existing.customRecordsSize() == 0) {
map.erase(found);
return true;
}
and then we need to remove only the right ones. */
bool result = false;
- for (const auto& toRemove : pol.d_custom) {
- for (auto it = existing.d_custom.begin(); it != existing.d_custom.end(); ++it) {
- if (**it == *toRemove) {
- existing.d_custom.erase(it);
- result = true;
- break;
+ if (pol.d_custom && existing.d_custom) {
+ for (const auto& toRemove : *pol.d_custom) {
+ for (auto it = existing.d_custom->begin(); it != existing.d_custom->end(); ++it) {
+ if (**it == *toRemove) {
+ existing.d_custom->erase(it);
+ result = true;
+ break;
+ }
}
}
}
// No records left for this trigger?
- if (existing.d_custom.empty()) {
+ if (existing.customRecordsSize() == 0) {
nmt.erase(netmask);
return true;
}
std::string DNSFilterEngine::Policy::getLogString() const
{
- return ": RPZ Hit; PolicyName=" + getName() + "; Trigger=" + d_trigger.toLogString() + "; Hit=" + d_hit + "; Type=" + getTypeToString(d_type) + "; Kind=" + getKindToString(d_kind);
+ return ": RPZ Hit; PolicyName=" + getName() + "; Trigger=" + getTrigger().toLogString() + "; Hit=" + getHit() + "; Type=" + getTypeToString(d_type) + "; Kind=" + getKindToString(d_kind);
}
void DNSFilterEngine::Policy::info(Logr::Priority prio, const std::shared_ptr<Logr::Logger>& log) const
{
- log->info(prio, "RPZ Hit", "policyName", Logging::Loggable(getName()), "trigger", Logging::Loggable(d_trigger),
- "hit", Logging::Loggable(d_hit), "type", Logging::Loggable(getTypeToString(d_type)),
+ log->info(prio, "RPZ Hit", "policyName", Logging::Loggable(getName()), "trigger", Logging::Loggable(getTrigger()),
+ "hit", Logging::Loggable(getHit()), "type", Logging::Loggable(getTypeToString(d_type)),
"kind", Logging::Loggable(getKindToString(d_kind)));
}
}
std::vector<DNSRecord> result;
+ if (customRecordsSize() == 0) {
+ return result;
+ }
- for (const auto& custom : d_custom) {
+ for (const auto& custom : *d_custom) {
if (qtype != QType::ANY && qtype != custom->getType() && custom->getType() != QType::CNAME) {
continue;
}
}
Policy(PolicyKind kind, PolicyType type, int32_t ttl = 0, std::shared_ptr<PolicyZoneData> data = nullptr, const std::vector<std::shared_ptr<const DNSRecordContent>>& custom = {}) :
- d_custom(custom), d_zoneData(std::move(data)), d_ttl(ttl), d_kind(kind), d_type(type)
+ d_zoneData(std::move(data)), d_custom(nullptr), d_ttl(ttl), d_kind(kind), d_type(type)
+ {
+ if (!custom.empty()) {
+ setCustom(custom);
+ }
+ }
+
+ ~Policy() = default;
+
+ Policy(const Policy& rhs) :
+ d_zoneData(rhs.d_zoneData),
+ d_custom(rhs.d_custom ? make_unique<CustomData>(*rhs.d_custom) : nullptr),
+ d_hitdata(rhs.d_hitdata ? make_unique<HitData>(*rhs.d_hitdata) : nullptr),
+ d_ttl(rhs.d_ttl),
+ d_kind(rhs.d_kind),
+ d_type(rhs.d_type)
{
}
+ Policy& operator=(const Policy& rhs)
+ {
+ if (this != &rhs) {
+ if (rhs.d_custom) {
+ d_custom = make_unique<CustomData>(*rhs.d_custom);
+ }
+ d_zoneData = rhs.d_zoneData;
+ if (rhs.d_hitdata) {
+ d_hitdata = make_unique<HitData>(*rhs.d_hitdata);
+ }
+ else {
+ d_hitdata = nullptr;
+ }
+ d_ttl = rhs.d_ttl;
+ d_kind = rhs.d_kind;
+ d_type = rhs.d_type;
+ }
+ return *this;
+ }
+
+ Policy(Policy&&) = default;
+ Policy& operator=(Policy&&) = default;
+
bool operator==(const Policy& rhs) const
{
return d_kind == rhs.d_kind && d_type == rhs.d_type && d_ttl == rhs.d_ttl && d_custom == rhs.d_custom;
[[nodiscard]] std::vector<DNSRecord> getCustomRecords(const DNSName& qname, uint16_t qtype) const;
[[nodiscard]] std::vector<DNSRecord> getRecords(const DNSName& qname) const;
- std::vector<std::shared_ptr<const DNSRecordContent>> d_custom;
std::shared_ptr<PolicyZoneData> d_zoneData{nullptr};
- DNSName d_trigger;
- string d_hit;
+
+ using CustomData = std::vector<std::shared_ptr<const DNSRecordContent>>;
+ std::unique_ptr<CustomData> d_custom;
+
+ struct HitData
+ {
+ DNSName d_trigger;
+ string d_hit;
+ };
+ std::unique_ptr<HitData> d_hitdata;
/* Yup, we are currently using the same TTL for every record for a given name */
int32_t d_ttl;
PolicyKind d_kind;
}
}
+ void setCustom(const CustomData& custom)
+ {
+ d_custom = make_unique<CustomData>(custom);
+ }
+
+ [[nodiscard]] size_t customRecordsSize() const
+ {
+ if (d_custom) {
+ return d_custom->size();
+ }
+ return 0;
+ }
+
+ void setHitData(const DNSName& name, const string& hit)
+ {
+ HitData hitdata{name, hit};
+ d_hitdata = make_unique<HitData>(hitdata);
+ }
+
+ [[nodiscard]] DNSName getTrigger() const
+ {
+ return d_hitdata ? d_hitdata->d_trigger : DNSName();
+ }
+
+ [[nodiscard]] std::string getHit() const
+ {
+ return d_hitdata ? d_hitdata->d_hit : "";
+ }
+
private:
[[nodiscard]] DNSRecord getRecordFromCustom(const DNSName& qname, const std::shared_ptr<const DNSRecordContent>& custom) const;
};
d_lw->registerMember("policyKind", &DNSFilterEngine::Policy::d_kind);
d_lw->registerMember("policyType", &DNSFilterEngine::Policy::d_type);
d_lw->registerMember("policyTTL", &DNSFilterEngine::Policy::d_ttl);
- d_lw->registerMember("policyTrigger", &DNSFilterEngine::Policy::d_trigger);
- d_lw->registerMember("policyHit", &DNSFilterEngine::Policy::d_hit);
+ d_lw->registerMember<DNSFilterEngine::Policy, DNSName>("policyTrigger",
+ [](const DNSFilterEngine::Policy& pol) {
+ return pol.getTrigger();
+ },
+ [](DNSFilterEngine::Policy& pol, const DNSName& dnsname) {
+ pol.setHitData(dnsname, pol.getHit());
+ });
+ d_lw->registerMember<DNSFilterEngine::Policy, std::string>("policyHit",
+ [](const DNSFilterEngine::Policy& pol) {
+ return pol.getHit();
+ },
+ [](DNSFilterEngine::Policy& pol, const std::string& hit) {
+ pol.setHitData(pol.getTrigger(), hit);
+ });
d_lw->registerMember<DNSFilterEngine::Policy, std::string>("policyCustom",
[](const DNSFilterEngine::Policy& pol) -> std::string {
std::string result;
return result;
}
- for (const auto& dr : pol.d_custom) {
- if (!result.empty()) {
- result += "\n";
+ if (pol.d_custom) {
+ for (const auto& dnsRecord : *pol.d_custom) {
+ if (!result.empty()) {
+ result += "\n";
+ }
+ result += dnsRecord->getZoneRepresentation();
}
- result += dr->getZoneRepresentation();
}
return result;
},
[](DNSFilterEngine::Policy& pol, const std::string& content) {
// Only CNAMES for now, when we ever add a d_custom_type, there will be pain
- pol.d_custom.clear();
- pol.d_custom.push_back(DNSRecordContent::make(QType::CNAME, QClass::IN, content));
+ pol.d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
+ pol.d_custom->push_back(DNSRecordContent::make(QType::CNAME, QClass::IN, content));
}
);
d_lw->registerFunction("getDH", &DNSQuestion::getDH);
break;
}
catch (const pdns::validation::TooManySEC3IterationsException& e) {
- if (g_logCommonErrors) {
+ if (g_logCommonErrors || (g_dnssecLogBogus && resolver.getDNSSECLimitHit())) {
SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << comboWriter->d_mdp.d_qname << "' because: " << e.what() << endl,
resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve of the custom filter policy",
- "policyName", Logging::Loggable(appliedPolicy.getName()), "exception", Logging::Loggable("TooManySEC3IterationsException")));
+ "policyName", Logging::Loggable(appliedPolicy.getName()), "exception", Logging::Loggable("TooManySEC3IterationsException"), "dnsseclimithit", Logging::Loggable(resolver.getDNSSECLimitHit())));
}
res = RCode::ServFail;
break;
catch (const pdns::validation::TooManySEC3IterationsException& e) {
if (g_logCommonErrors) {
SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during resolve of '" << comboWriter->d_mdp.d_qname << "' because: " << e.what() << endl,
- resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve"));
+ resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve", "dnsseclimithit", Logging::Loggable(true)));
}
res = RCode::ServFail;
}
if (resolver.doLog() || vStateIsBogus(state)) {
// Only create logging object if needed below, beware if you change the logging logic!
log = resolver.d_slog->withValues("vstate", Logging::Loggable(state));
+ if (resolver.getDNSSECLimitHit()) {
+ log = log->withValues("dnsseclimithit", Logging::Loggable(true));
+ }
auto xdnssec = g_xdnssec.getLocal();
if (xdnssec->check(comboWriter->d_mdp.d_qname)) {
log = log->withValues("in-x-dnssec-names", Logging::Loggable(1));
goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
}
catch (const pdns::validation::TooManySEC3IterationsException& e) {
- if (g_logCommonErrors) {
+ if (g_logCommonErrors || (g_dnssecLogBogus && resolver.getDNSSECLimitHit())) {
SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during validation of '" << comboWriter->d_mdp.d_qname << "|" << QType(comboWriter->d_mdp.d_qtype) << "' because: " << e.what() << endl,
- resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException")));
+ resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException"), "dnsseclimithit", Logging::Loggable(resolver.getDNSSECLimitHit())));
}
goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
}
if (!appliedPolicy.getName().empty()) {
pbMessage.setAppliedPolicy(appliedPolicy.getName());
pbMessage.setAppliedPolicyType(appliedPolicy.d_type);
- pbMessage.setAppliedPolicyTrigger(appliedPolicy.d_trigger);
- pbMessage.setAppliedPolicyHit(appliedPolicy.d_hit);
+ pbMessage.setAppliedPolicyTrigger(appliedPolicy.getTrigger());
+ pbMessage.setAppliedPolicyHit(appliedPolicy.getHit());
pbMessage.setAppliedPolicyKind(appliedPolicy.d_kind);
}
pbMessage.setInBytes(packet.size());
defpol->d_kind = (DNSFilterEngine::PolicyKind)boost::get<uint32_t>(have["defpol"]);
defpol->setName(polName);
if (defpol->d_kind == DNSFilterEngine::PolicyKind::Custom) {
- defpol->d_custom.push_back(DNSRecordContent::make(QType::CNAME, QClass::IN,
- boost::get<string>(have["defcontent"])));
+ if (!defpol->d_custom) {
+ defpol->d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
+ }
+ defpol->d_custom->push_back(DNSRecordContent::make(QType::CNAME, QClass::IN,
+ boost::get<string>(have["defcontent"])));
if (have.count("defttl") != 0) {
defpol->d_ttl = static_cast<int32_t>(boost::get<uint32_t>(have["defttl"]));
}
}
- unsigned int ringsize = ::arg().asNum("stats-ringbuffer-entries") / RecThreadInfo::numUDPWorkers();
- if (ringsize != 0) {
+ if (unsigned int ringsize = ::arg().asNum("stats-ringbuffer-entries") / RecThreadInfo::numUDPWorkers(); ringsize != 0) {
t_remotes = std::make_unique<addrringbuf_t>();
if (RecThreadInfo::weDistributeQueries()) {
t_remotes->set_capacity(::arg().asNum("stats-ringbuffer-entries") / RecThreadInfo::numDistributors());
g_multiTasker = std::make_unique<MT_t>(::arg().asNum("stack-size"), ::arg().asNum("stack-cache-size"));
threadInfo.setMT(g_multiTasker.get());
- /* start protobuf export threads if needed */
- auto luaconfsLocal = g_luaconfs.getLocal();
- checkProtobufExport(luaconfsLocal);
- checkOutgoingProtobufExport(luaconfsLocal);
+ {
+ /* start protobuf export threads if needed, don;'t keep a ref to lua config around */
+ auto luaconfsLocal = g_luaconfs.getLocal();
+ checkProtobufExport(luaconfsLocal);
+ checkOutgoingProtobufExport(luaconfsLocal);
#ifdef HAVE_FSTRM
- checkFrameStreamExport(luaconfsLocal, luaconfsLocal->frameStreamExportConfig, t_frameStreamServersInfo);
- checkFrameStreamExport(luaconfsLocal, luaconfsLocal->nodFrameStreamExportConfig, t_nodFrameStreamServersInfo);
+ checkFrameStreamExport(luaconfsLocal, luaconfsLocal->frameStreamExportConfig, t_frameStreamServersInfo);
+ checkFrameStreamExport(luaconfsLocal, luaconfsLocal->nodFrameStreamExportConfig, t_nodFrameStreamServersInfo);
#endif
+ }
t_fdm = unique_ptr<FDMultiplexer>(getMultiplexer(log));
d_now = time(nullptr);
for (const auto& [key, v] : d_all) {
const auto& [qname, qtype] = key;
+ if (qname.isWildcard()) {
+ continue;
+ }
switch (qtype) {
case QType::NSEC:
case QType::NSEC3:
* $NETMASK.zz (::/$NETMASK)
* Terrible right?
*/
- if (parts.size() < 2 || parts.size() > 9)
+ if (parts.size() < 2 || parts.size() > 9) {
throw PDNSException("Invalid IP address in RPZ: " + name.toLogString());
+ }
bool isV6 = (stoi(parts[0]) > 32);
bool hadZZ = false;
for (auto& part : parts) {
// Check if we have an IPv4 octet
- for (auto c : part)
- if (!isdigit(c))
+ for (auto labelLetter : part) {
+ if (isdigit(labelLetter) == 0) {
isV6 = true;
-
+ }
+ }
if (pdns_iequals(part, "zz")) {
- if (hadZZ)
+ if (hadZZ) {
throw PDNSException("more than one 'zz' label found in RPZ name" + name.toLogString());
+ }
part = "";
isV6 = true;
hadZZ = true;
}
}
- if (isV6 && parts.size() < 9 && !hadZZ)
+ if (isV6 && parts.size() < 9 && !hadZZ) {
throw PDNSException("No 'zz' label found in an IPv6 RPZ name shorter than 9 elements: " + name.toLogString());
+ }
- if (parts.size() == 5 && !isV6)
- return Netmask(parts[4] + "." + parts[3] + "." + parts[2] + "." + parts[1] + "/" + parts[0]);
-
- string v6;
+ if (parts.size() == 5 && !isV6) {
+ return parts[4] + "." + parts[3] + "." + parts[2] + "." + parts[1] + "/" + parts[0];
+ }
+ string v6Address;
- if (parts[parts.size() - 1] == "") {
- v6 += ":";
+ if (parts[parts.size() - 1].empty()) {
+ v6Address += ":";
}
for (uint8_t i = parts.size() - 1; i > 0; i--) {
- v6 += parts[i];
- if (i > 1 || (i == 1 && parts[i] == "")) {
- v6 += ":";
+ v6Address += parts[i];
+ if (i > 1 || (i == 1 && parts[i].empty())) {
+ v6Address += ":";
}
}
- v6 += "/" + parts[0];
+ v6Address += "/" + parts[0];
- return Netmask(v6);
+ return v6Address;
}
-static void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptr<DNSFilterEngine::Zone> zone, bool addOrRemove, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, Logr::log_t log)
+static void RPZRecordToPolicy(const DNSRecord& dnsRecord, const std::shared_ptr<DNSFilterEngine::Zone>& zone, bool addOrRemove, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, Logr::log_t log)
{
- static const DNSName drop("rpz-drop."), truncate("rpz-tcp-only."), noaction("rpz-passthru.");
- static const DNSName rpzClientIP("rpz-client-ip"), rpzIP("rpz-ip"),
- rpzNSDname("rpz-nsdname"), rpzNSIP("rpz-nsip.");
+ static const DNSName drop("rpz-drop.");
+ static const DNSName truncate("rpz-tcp-only.");
+ static const DNSName noaction("rpz-passthru.");
+ static const DNSName rpzClientIP("rpz-client-ip");
+ static const DNSName rpzIP("rpz-ip");
+ static const DNSName rpzNSDname("rpz-nsdname");
+ static const DNSName rpzNSIP("rpz-nsip.");
static const std::string rpzPrefix("rpz-");
DNSFilterEngine::Policy pol;
bool defpolApplied = false;
- if (dr.d_class != QClass::IN) {
+ if (dnsRecord.d_class != QClass::IN) {
return;
}
- if (dr.d_type == QType::CNAME) {
- auto crc = getRR<CNAMERecordContent>(dr);
+ if (dnsRecord.d_type == QType::CNAME) {
+ auto crc = getRR<CNAMERecordContent>(dnsRecord);
if (!crc) {
return;
}
else if (!crcTarget.empty() && !crcTarget.isRoot() && crcTarget.getRawLabel(crcTarget.countLabels() - 1).compare(0, rpzPrefix.length(), rpzPrefix) == 0) {
/* this is very likely an higher format number or a configuration error,
let's just ignore it. */
- SLOG(g_log << Logger::Info << "Discarding unsupported RPZ entry " << crcTarget << " for " << dr.d_name << endl,
- log->info(Logr::Info, "Discarding unsupported RPZ entry", "target", Logging::Loggable(crcTarget), "name", Logging::Loggable(dr.d_name)));
+ SLOG(g_log << Logger::Info << "Discarding unsupported RPZ entry " << crcTarget << " for " << dnsRecord.d_name << endl,
+ log->info(Logr::Info, "Discarding unsupported RPZ entry", "target", Logging::Loggable(crcTarget), "name", Logging::Loggable(dnsRecord.d_name)));
return;
}
else {
pol.d_kind = DNSFilterEngine::PolicyKind::Custom;
- pol.d_custom.emplace_back(dr.getContent());
+ if (!pol.d_custom) {
+ pol.d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
+ }
+ pol.d_custom->emplace_back(dnsRecord.getContent());
// cerr<<"Wants custom "<<crcTarget<<" for "<<dr.d_name<<": ";
}
}
}
else {
pol.d_kind = DNSFilterEngine::PolicyKind::Custom;
- pol.d_custom.emplace_back(dr.getContent());
+ if (!pol.d_custom) {
+ pol.d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
+ }
+ pol.d_custom->emplace_back(dnsRecord.getContent());
// cerr<<"Wants custom "<<dr.d_content->getZoneRepresentation()<<" for "<<dr.d_name<<": ";
}
}
if (!defpolApplied || defpol->d_ttl < 0) {
- pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, dr.d_ttl));
+ pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, dnsRecord.d_ttl));
}
else {
pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, static_cast<uint32_t>(pol.d_ttl)));
// now to DO something with that
- if (dr.d_name.isPartOf(rpzNSDname)) {
- DNSName filt = dr.d_name.makeRelative(rpzNSDname);
- if (addOrRemove)
+ if (dnsRecord.d_name.isPartOf(rpzNSDname)) {
+ DNSName filt = dnsRecord.d_name.makeRelative(rpzNSDname);
+ if (addOrRemove) {
zone->addNSTrigger(filt, std::move(pol), defpolApplied);
- else
- zone->rmNSTrigger(filt, std::move(pol));
- }
- else if (dr.d_name.isPartOf(rpzClientIP)) {
- DNSName filt = dr.d_name.makeRelative(rpzClientIP);
- auto nm = makeNetmaskFromRPZ(filt);
- if (addOrRemove)
- zone->addClientTrigger(nm, std::move(pol), defpolApplied);
- else
- zone->rmClientTrigger(nm, std::move(pol));
- }
- else if (dr.d_name.isPartOf(rpzIP)) {
+ }
+ else {
+ zone->rmNSTrigger(filt, pol);
+ }
+ }
+ else if (dnsRecord.d_name.isPartOf(rpzClientIP)) {
+ DNSName filt = dnsRecord.d_name.makeRelative(rpzClientIP);
+ auto netmask = makeNetmaskFromRPZ(filt);
+ if (addOrRemove) {
+ zone->addClientTrigger(netmask, std::move(pol), defpolApplied);
+ }
+ else {
+ zone->rmClientTrigger(netmask, pol);
+ }
+ }
+ else if (dnsRecord.d_name.isPartOf(rpzIP)) {
// cerr<<"Should apply answer content IP policy: "<<dr.d_name<<endl;
- DNSName filt = dr.d_name.makeRelative(rpzIP);
- auto nm = makeNetmaskFromRPZ(filt);
- if (addOrRemove)
- zone->addResponseTrigger(nm, std::move(pol), defpolApplied);
- else
- zone->rmResponseTrigger(nm, std::move(pol));
- }
- else if (dr.d_name.isPartOf(rpzNSIP)) {
- DNSName filt = dr.d_name.makeRelative(rpzNSIP);
- auto nm = makeNetmaskFromRPZ(filt);
- if (addOrRemove)
- zone->addNSIPTrigger(nm, std::move(pol), defpolApplied);
- else
- zone->rmNSIPTrigger(nm, std::move(pol));
+ DNSName filt = dnsRecord.d_name.makeRelative(rpzIP);
+ auto netmask = makeNetmaskFromRPZ(filt);
+ if (addOrRemove) {
+ zone->addResponseTrigger(netmask, std::move(pol), defpolApplied);
+ }
+ else {
+ zone->rmResponseTrigger(netmask, pol);
+ }
+ }
+ else if (dnsRecord.d_name.isPartOf(rpzNSIP)) {
+ DNSName filt = dnsRecord.d_name.makeRelative(rpzNSIP);
+ auto netmask = makeNetmaskFromRPZ(filt);
+ if (addOrRemove) {
+ zone->addNSIPTrigger(netmask, std::move(pol), defpolApplied);
+ }
+ else {
+ zone->rmNSIPTrigger(netmask, pol);
+ }
}
else {
if (addOrRemove) {
/* if we did override the existing policy with the default policy,
we might turn two A or AAAA into a CNAME, which would trigger
an exception. Let's just ignore it. */
- zone->addQNameTrigger(dr.d_name, std::move(pol), defpolApplied);
+ zone->addQNameTrigger(dnsRecord.d_name, std::move(pol), defpolApplied);
}
else {
- zone->rmQNameTrigger(dr.d_name, std::move(pol));
+ zone->rmQNameTrigger(dnsRecord.d_name, pol);
}
}
}
-static shared_ptr<const SOARecordContent> loadRPZFromServer(Logr::log_t plogger, const ComboAddress& primary, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone> zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout)
+static shared_ptr<const SOARecordContent> loadRPZFromServer(Logr::log_t plogger, const ComboAddress& primary, const DNSName& zoneName, const std::shared_ptr<DNSFilterEngine::Zone>& zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, const TSIGTriplet& tsigTriplet, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout)
{
auto logger = plogger->withValues("primary", Logging::Loggable(primary));
SLOG(g_log << Logger::Warning << "Loading RPZ zone '" << zoneName << "' from " << primary.toStringWithPort() << endl,
logger->info(Logr::Info, "Loading RPZ from nameserver"));
- if (!tt.name.empty()) {
- SLOG(g_log << Logger::Warning << "With TSIG key '" << tt.name << "' of algorithm '" << tt.algo << "'" << endl,
- logger->info(Logr::Info, "Using TSIG key for authentication", "tsig_key_name", Logging::Loggable(tt.name), "tsig_key_algorithm", Logging::Loggable(tt.algo)));
+ if (!tsigTriplet.name.empty()) {
+ SLOG(g_log << Logger::Warning << "With TSIG key '" << tsigTriplet.name << "' of algorithm '" << tsigTriplet.algo << "'" << endl,
+ logger->info(Logr::Info, "Using TSIG key for authentication", "tsig_key_name", Logging::Loggable(tsigTriplet.name), "tsig_key_algorithm", Logging::Loggable(tsigTriplet.algo)));
}
ComboAddress local(localAddress);
- if (local == ComboAddress())
+ if (local == ComboAddress()) {
local = pdns::getQueryLocalAddress(primary.sin4.sin_family, 0);
+ }
- AXFRRetriever axfr(primary, zoneName, tt, &local, maxReceivedBytes, axfrTimeout);
+ AXFRRetriever axfr(primary, zoneName, tsigTriplet, &local, maxReceivedBytes, axfrTimeout);
unsigned int nrecords = 0;
Resolver::res_t nop;
vector<DNSRecord> chunk;
time_t last = 0;
time_t axfrStart = time(nullptr);
time_t axfrNow = time(nullptr);
- shared_ptr<const SOARecordContent> sr;
+ shared_ptr<const SOARecordContent> soaRecordContent;
// coverity[store_truncates_time_t]
- while (axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow))) {
- for (auto& dr : chunk) {
- if (dr.d_type == QType::NS || dr.d_type == QType::TSIG) {
+ while (axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow)) != 0) {
+ for (auto& dnsRecord : chunk) {
+ if (dnsRecord.d_type == QType::NS || dnsRecord.d_type == QType::TSIG) {
continue;
}
- dr.d_name.makeUsRelative(zoneName);
- if (dr.d_type == QType::SOA) {
- sr = getRR<SOARecordContent>(dr);
- zone->setSOA(dr);
+ dnsRecord.d_name.makeUsRelative(zoneName);
+ if (dnsRecord.d_type == QType::SOA) {
+ soaRecordContent = getRR<SOARecordContent>(dnsRecord);
+ zone->setSOA(dnsRecord);
continue;
}
- RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL, logger);
+ RPZRecordToPolicy(dnsRecord, zone, true, defpol, defpolOverrideLocal, maxTTL, logger);
nrecords++;
}
axfrNow = time(nullptr);
if (axfrNow < axfrStart || axfrNow - axfrStart > axfrTimeout) {
throw PDNSException("Total AXFR time exceeded!");
}
- if (last != time(0)) {
+ if (last != time(nullptr)) {
SLOG(g_log << Logger::Info << "Loaded & indexed " << nrecords << " policy records so far for RPZ zone '" << zoneName << "'" << endl,
logger->info(Logr::Info, "RPZ load in progress", "nrecords", Logging::Loggable(nrecords)));
- last = time(0);
+ last = time(nullptr);
}
}
- SLOG(g_log << Logger::Info << "Done: " << nrecords << " policy records active, SOA: " << sr->getZoneRepresentation() << endl,
- logger->info(Logr::Info, "RPZ load completed", "nrecords", Logging::Loggable(nrecords), "soa", Logging::Loggable(sr->getZoneRepresentation())));
- return sr;
+ SLOG(g_log << Logger::Info << "Done: " << nrecords << " policy records active, SOA: " << soaRecordContent->getZoneRepresentation() << endl,
+ logger->info(Logr::Info, "RPZ load completed", "nrecords", Logging::Loggable(nrecords), "soa", Logging::Loggable(soaRecordContent->getZoneRepresentation())));
+ return soaRecordContent;
}
static LockGuarded<std::unordered_map<std::string, shared_ptr<rpzStats>>> s_rpzStats;
shared_ptr<rpzStats> getRPZZoneStats(const std::string& zone)
{
auto stats = s_rpzStats.lock();
- auto it = stats->find(zone);
- if (it == stats->end()) {
+ auto statsIt = stats->find(zone);
+ if (statsIt == stats->end()) {
auto stat = std::make_shared<rpzStats>();
(*stats)[zone] = stat;
return stat;
}
- return it->second;
+ return statsIt->second;
}
static void incRPZFailedTransfers(const std::string& zone)
{
auto stats = getRPZZoneStats(zone);
- if (stats != nullptr)
+ if (stats != nullptr) {
stats->d_failedTransfers++;
+ }
}
static void setRPZZoneNewState(const std::string& zone, uint32_t serial, uint64_t numberOfRecords, bool fromFile, bool wasAXFR)
}
// this function is silent - you do the logging
-std::shared_ptr<const SOARecordContent> loadRPZFromFile(const std::string& fname, std::shared_ptr<DNSFilterEngine::Zone> zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL)
+std::shared_ptr<const SOARecordContent> loadRPZFromFile(const std::string& fname, const std::shared_ptr<DNSFilterEngine::Zone>& zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL)
{
- shared_ptr<const SOARecordContent> sr = nullptr;
+ shared_ptr<const SOARecordContent> soaRecordContent = nullptr;
ZoneParserTNG zpt(fname);
zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
auto log = g_slog->withName("rpz")->withValues("file", Logging::Loggable(fname), "zone", Logging::Loggable(zone->getName()));
while (zpt.get(drr)) {
try {
- if (drr.qtype.getCode() == QType::CNAME && drr.content.empty())
+ if (drr.qtype.getCode() == QType::CNAME && drr.content.empty()) {
drr.content = ".";
- DNSRecord dr(drr);
- if (dr.d_type == QType::SOA) {
- sr = getRR<SOARecordContent>(dr);
- domain = dr.d_name;
+ }
+ DNSRecord dnsRecord(drr);
+ if (dnsRecord.d_type == QType::SOA) {
+ soaRecordContent = getRR<SOARecordContent>(dnsRecord);
+ domain = dnsRecord.d_name;
zone->setDomain(domain);
- soaRecord = std::move(dr);
+ soaRecord = std::move(dnsRecord);
}
- else if (dr.d_type == QType::NS) {
+ else if (dnsRecord.d_type == QType::NS) {
continue;
}
else {
- dr.d_name = dr.d_name.makeRelative(domain);
- RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL, log);
+ dnsRecord.d_name = dnsRecord.d_name.makeRelative(domain);
+ RPZRecordToPolicy(dnsRecord, zone, true, defpol, defpolOverrideLocal, maxTTL, log);
}
}
catch (const PDNSException& pe) {
}
}
- if (sr != nullptr) {
- zone->setRefresh(sr->d_st.refresh);
+ if (soaRecordContent != nullptr) {
+ zone->setRefresh(soaRecordContent->d_st.refresh);
zone->setSOA(std::move(soaRecord));
- setRPZZoneNewState(zone->getName(), sr->d_st.serial, zone->size(), true, false);
+ setRPZZoneNewState(zone->getName(), soaRecordContent->d_st.serial, zone->size(), true, false);
}
- return sr;
+ return soaRecordContent;
}
static bool dumpZoneToDisk(Logr::log_t logger, const DNSName& zoneName, const std::shared_ptr<DNSFilterEngine::Zone>& newZone, const std::string& dumpZoneFileName)
{
logger->info(Logr::Debug, "Dumping zone to disk", "destination_file", Logging::Loggable(dumpZoneFileName));
std::string temp = dumpZoneFileName + "XXXXXX";
- int fd = mkstemp(&temp.at(0));
- if (fd < 0) {
+ int fileDesc = mkstemp(&temp.at(0));
+ if (fileDesc < 0) {
SLOG(g_log << Logger::Warning << "Unable to open a file to dump the content of the RPZ zone " << zoneName << endl,
logger->error(Logr::Error, errno, "Unable to create temporary file"));
return false;
}
- auto fp = std::unique_ptr<FILE, int (*)(FILE*)>(fdopen(fd, "w+"), fclose);
- if (!fp) {
+ auto filePtr = std::unique_ptr<FILE, int (*)(FILE*)>(fdopen(fileDesc, "w+"), fclose);
+ if (!filePtr) {
int err = errno;
- close(fd);
+ close(fileDesc);
SLOG(g_log << Logger::Warning << "Unable to open a file pointer to dump the content of the RPZ zone " << zoneName << endl,
logger->error(Logr::Error, err, "Unable to open file pointer"));
return false;
}
try {
- newZone->dump(fp.get());
+ newZone->dump(filePtr.get());
}
catch (const std::exception& e) {
SLOG(g_log << Logger::Warning << "Error while dumping the content of the RPZ zone " << zoneName << ": " << e.what() << endl,
return false;
}
- if (fflush(fp.get()) != 0) {
+ if (fflush(filePtr.get()) != 0) {
SLOG(g_log << Logger::Warning << "Error while flushing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
logger->error(Logr::Warning, errno, "Error while flushing the content of the RPZ"));
return false;
}
- if (fsync(fileno(fp.get())) != 0) {
+ if (fsync(fileno(filePtr.get())) != 0) {
SLOG(g_log << Logger::Warning << "Error while syncing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
logger->error(Logr::Error, errno, "Error while syncing the content of the RPZ"));
return false;
}
- if (fclose(fp.release()) != 0) {
+ if (fclose(filePtr.release()) != 0) {
SLOG(g_log << Logger::Warning << "Error while writing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
logger->error(Logr::Error, errno, "Error while writing the content of the RPZ"));
return false;
std::string dumpZoneFileName;
};
-std::shared_ptr<const SOARecordContent> loadRPZFromFile(const std::string& fname, std::shared_ptr<DNSFilterEngine::Zone> zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL);
+std::shared_ptr<const SOARecordContent> loadRPZFromFile(const std::string& fname, const std::shared_ptr<DNSFilterEngine::Zone>& zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL);
void RPZIXFRTracker(RPZTrackerParams params, uint64_t configGeneration);
bool notifyRPZTracker(const DNSName& name);
sanitizeRecords(prefix, lwr, qname, qtype, auth, wasForwarded, rdQuery);
std::vector<std::shared_ptr<DNSRecord>> authorityRecs;
- const unsigned int labelCount = qname.countLabels();
bool isCNAMEAnswer = false;
bool isDNAMEAnswer = false;
DNSName seenAuth;
+ // names that might be expanded from a wildcard, and thus require denial of existence proof
+ // this is the queried name and any part of the CNAME chain from the queried name
+ // the key is the name itself, the value is initially false and is set to true once we have
+ // confirmed it was actually expanded from a wildcard
+ std::map<DNSName, bool> wildcardCandidates{{qname, false}};
+
+ if (rdQuery) {
+ std::unordered_map<DNSName, DNSName> cnames;
+ for (const auto& rec : lwr.d_records) {
+ if (rec.d_type != QType::CNAME || rec.d_class != QClass::IN) {
+ continue;
+ }
+ if (auto content = getRR<CNAMERecordContent>(rec)) {
+ cnames[rec.d_name] = DNSName(content->getTarget());
+ }
+ }
+ auto initial = qname;
+ while (true) {
+ auto cnameIt = cnames.find(initial);
+ if (cnameIt == cnames.end()) {
+ break;
+ }
+ initial = cnameIt->second;
+ wildcardCandidates.emplace(initial, false);
+ }
+ }
+
for (auto& rec : lwr.d_records) {
if (rec.d_type == QType::OPT || rec.d_class != QClass::IN) {
continue;
seenAuth = rec.d_name;
}
+ const auto labelCount = rec.d_name.countLabels();
if (rec.d_type == QType::RRSIG) {
auto rrsig = getRR<RRSIGRecordContent>(rec);
if (rrsig) {
count can be lower than the name's label count if it was
synthesized from the wildcard. Note that the difference might
be > 1. */
- if (rec.d_name == qname && isWildcardExpanded(labelCount, *rrsig)) {
+ if (auto wcIt = wildcardCandidates.find(rec.d_name); wcIt != wildcardCandidates.end() && isWildcardExpanded(labelCount, *rrsig)) {
+ wcIt->second = true;
gatherWildcardProof = true;
if (!isWildcardExpandedOntoItself(rec.d_name, labelCount, *rrsig)) {
/* if we have a wildcard expanded onto itself, we don't need to prove
if (isAA && tCacheEntry->first.type == QType::NS && s_save_parent_ns_set) {
rememberParentSetIfNeeded(tCacheEntry->first.name, tCacheEntry->second.records, depth, prefix);
}
- g_recCache->replace(d_now.tv_sec, tCacheEntry->first.name, tCacheEntry->first.type, tCacheEntry->second.records, tCacheEntry->second.signatures, authorityRecs, tCacheEntry->first.type == QType::DS ? true : isAA, auth, tCacheEntry->first.place == DNSResourceRecord::ANSWER ? ednsmask : boost::none, d_routingTag, recordState, remoteIP, d_refresh, tCacheEntry->second.d_ttl_time);
+ bool thisRRNeedsWildcardProof = false;
+ if (gatherWildcardProof) {
+ if (auto wcIt = wildcardCandidates.find(tCacheEntry->first.name); wcIt != wildcardCandidates.end() && wcIt->second) {
+ thisRRNeedsWildcardProof = true;
+ }
+ }
+ g_recCache->replace(d_now.tv_sec, tCacheEntry->first.name, tCacheEntry->first.type, tCacheEntry->second.records, tCacheEntry->second.signatures, thisRRNeedsWildcardProof ? authorityRecs : std::vector<std::shared_ptr<DNSRecord>>(), tCacheEntry->first.type == QType::DS ? true : isAA, auth, tCacheEntry->first.place == DNSResourceRecord::ANSWER ? ednsmask : boost::none, d_routingTag, recordState, remoteIP, d_refresh, tCacheEntry->second.d_ttl_time);
// Delete potential negcache entry. When a record recovers with serve-stale the negcache entry can cause the wrong entry to
// be served, as negcache entries are checked before record cache entries
g_negCache->wipeTyped(tCacheEntry->first.name, tCacheEntry->first.type);
}
- if (g_aggressiveNSECCache && needWildcardProof && recordState == vState::Secure && tCacheEntry->first.place == DNSResourceRecord::ANSWER && tCacheEntry->first.name == qname && !tCacheEntry->second.signatures.empty() && !d_routingTag && !ednsmask) {
+ if (g_aggressiveNSECCache && thisRRNeedsWildcardProof && recordState == vState::Secure && tCacheEntry->first.place == DNSResourceRecord::ANSWER && !tCacheEntry->second.signatures.empty() && !d_routingTag && !ednsmask) {
/* we have an answer synthesized from a wildcard and aggressive NSEC is enabled, we need to store the
wildcard in its non-expanded form in the cache to be able to synthesize wildcard answers later */
const auto& rrsig = tCacheEntry->second.signatures.at(0);
+ const auto labelCount = tCacheEntry->first.name.countLabels();
if (isWildcardExpanded(labelCount, *rrsig) && !isWildcardExpandedOntoItself(tCacheEntry->first.name, labelCount, *rrsig)) {
DNSName realOwner = getNSECOwnerName(tCacheEntry->first.name, tCacheEntry->second.signatures);
}
}
+ if (gatherWildcardProof) {
+ if (auto wcIt = wildcardCandidates.find(qname); wcIt != wildcardCandidates.end() && !wcIt->second) {
+ // the queried name was not expanded from a wildcard, a record in the CNAME chain was, so we don't need to gather wildcard proof now: we will do that when looking up the CNAME chain
+ gatherWildcardProof = false;
+ }
+ }
+
return RCode::NoError;
}
return d_queryValidationState;
}
+ [[nodiscard]] bool getDNSSECLimitHit() const
+ {
+ return d_validationContext.d_limitHit;
+ }
+
void setQueryReceivedOverTCP(bool tcp)
{
d_queryReceivedOverTCP = tcp;
const auto matchingPolicy = dfe.getProcessingPolicy(DNSName("sub.sub.wildcard.wolf."), std::unordered_map<std::string, bool>(), DNSFilterEngine::maximumPriority);
BOOST_CHECK(matchingPolicy.d_type == DNSFilterEngine::PolicyType::NSDName);
BOOST_CHECK(matchingPolicy.d_kind == DNSFilterEngine::PolicyKind::Drop);
- BOOST_CHECK_EQUAL(matchingPolicy.d_trigger, DNSName("*.wildcard.wolf.rpz-nsdname"));
- BOOST_CHECK_EQUAL(matchingPolicy.d_hit, "sub.sub.wildcard.wolf");
+ BOOST_CHECK_EQUAL(matchingPolicy.d_hitdata->d_trigger, DNSName("*.wildcard.wolf.rpz-nsdname"));
+ BOOST_CHECK_EQUAL(matchingPolicy.d_hitdata->d_hit, "sub.sub.wildcard.wolf");
/* looking for wildcard.wolf. should not match *.wildcard-blocked. */
const auto notMatchingPolicy = dfe.getProcessingPolicy(DNSName("wildcard.wolf."), std::unordered_map<std::string, bool>(), DNSFilterEngine::maximumPriority);
/* except if we look exactly for the wildcard */
BOOST_CHECK(zone->findExactNSPolicy(nsWildcardName, zonePolicy));
BOOST_CHECK(zonePolicy == matchingPolicy);
- BOOST_CHECK_EQUAL(zonePolicy.d_trigger, DNSName("*.wildcard.wolf.rpz-nsdname"));
- BOOST_CHECK_EQUAL(zonePolicy.d_hit, nsWildcardName.toStringNoDot());
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_trigger, DNSName("*.wildcard.wolf.rpz-nsdname"));
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_hit, nsWildcardName.toStringNoDot());
}
{
DNSFilterEngine::Policy zonePolicy;
BOOST_CHECK(zone->findNSIPPolicy(nsIP, zonePolicy));
BOOST_CHECK(zonePolicy == matchingPolicy);
- BOOST_CHECK_EQUAL(zonePolicy.d_trigger, DNSName("31.0.2.0.192.rpz-nsip"));
- BOOST_CHECK_EQUAL(zonePolicy.d_hit, nsIP.toString());
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_trigger, DNSName("31.0.2.0.192.rpz-nsip"));
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_hit, nsIP.toString());
}
{
DNSFilterEngine::Policy zonePolicy;
BOOST_CHECK(zone->findExactQNamePolicy(blockedName, zonePolicy));
BOOST_CHECK(zonePolicy == matchingPolicy);
- BOOST_CHECK_EQUAL(zonePolicy.d_trigger, blockedName);
- BOOST_CHECK_EQUAL(zonePolicy.d_hit, blockedName.toStringNoDot());
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_trigger, blockedName);
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_hit, blockedName.toStringNoDot());
/* but a subdomain should not be blocked (not a wildcard, and this is not suffix domain matching */
matchingPolicy = dfe.getQueryPolicy(DNSName("sub") + blockedName, std::unordered_map<std::string, bool>(), DNSFilterEngine::maximumPriority);
const auto matchingPolicy = dfe.getQueryPolicy(DNSName("sub.sub.wildcard-blocked."), std::unordered_map<std::string, bool>(), DNSFilterEngine::maximumPriority);
BOOST_CHECK(matchingPolicy.d_type == DNSFilterEngine::PolicyType::QName);
BOOST_CHECK(matchingPolicy.d_kind == DNSFilterEngine::PolicyKind::Drop);
- BOOST_CHECK_EQUAL(matchingPolicy.d_trigger, blockedWildcardName);
- BOOST_CHECK_EQUAL(matchingPolicy.d_hit, "sub.sub.wildcard-blocked");
+ BOOST_CHECK_EQUAL(matchingPolicy.d_hitdata->d_trigger, blockedWildcardName);
+ BOOST_CHECK_EQUAL(matchingPolicy.d_hitdata->d_hit, "sub.sub.wildcard-blocked");
/* looking for wildcard-blocked. should not match *.wildcard-blocked. */
const auto notMatchingPolicy = dfe.getQueryPolicy(DNSName("wildcard-blocked."), std::unordered_map<std::string, bool>(), DNSFilterEngine::maximumPriority);
/* except if we look exactly for the wildcard */
BOOST_CHECK(zone->findExactQNamePolicy(blockedWildcardName, zonePolicy));
BOOST_CHECK(zonePolicy == matchingPolicy);
- BOOST_CHECK_EQUAL(zonePolicy.d_trigger, blockedWildcardName);
- BOOST_CHECK_EQUAL(zonePolicy.d_hit, blockedWildcardName.toStringNoDot());
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_trigger, blockedWildcardName);
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_hit, blockedWildcardName.toStringNoDot());
}
{
DNSFilterEngine::Policy zonePolicy;
BOOST_CHECK(zone->findClientPolicy(clientIP, zonePolicy));
BOOST_CHECK(zonePolicy == matchingPolicy);
- BOOST_CHECK_EQUAL(zonePolicy.d_trigger, DNSName("31.128.2.0.192.rpz-client-ip"));
- BOOST_CHECK_EQUAL(zonePolicy.d_hit, clientIP.toString());
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_trigger, DNSName("31.128.2.0.192.rpz-client-ip"));
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_hit, clientIP.toString());
}
{
DNSFilterEngine::Policy zonePolicy;
BOOST_CHECK(zone->findResponsePolicy(responseIP, zonePolicy));
BOOST_CHECK(zonePolicy == matchingPolicy);
- BOOST_CHECK_EQUAL(zonePolicy.d_trigger, DNSName("31.254.2.0.192.rpz-ip"));
- BOOST_CHECK_EQUAL(zonePolicy.d_hit, responseIP.toString());
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_trigger, DNSName("31.254.2.0.192.rpz-ip"));
+ BOOST_CHECK_EQUAL(zonePolicy.d_hitdata->d_hit, responseIP.toString());
}
{
BOOST_CHECK_EQUAL(queriesCount, 4U);
}
+BOOST_AUTO_TEST_CASE(test_forward_zone_recurse_rd_dnssec_cname_wildcard_expanded)
+{
+ std::unique_ptr<SyncRes> testSR;
+ initSR(testSR, true);
+
+ setDNSSECValidation(testSR, DNSSECMode::ValidateAll);
+
+ primeHints();
+ /* unsigned */
+ const DNSName target("test.");
+ /* signed */
+ const DNSName cnameTarget("cname.");
+ testkeysset_t keys;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+ generateKeyMaterial(cnameTarget, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+ g_luaconfs.setState(luaconfsCopy);
+
+ const ComboAddress forwardedNS("192.0.2.42:53");
+ size_t queriesCount = 0;
+
+ SyncRes::AuthDomain authDomain;
+ authDomain.d_rdForward = true;
+ authDomain.d_servers.push_back(forwardedNS);
+ (*SyncRes::t_sstorage.domainmap)[g_rootdnsname] = authDomain;
+
+ testSR->setAsyncCallback([&](const ComboAddress& address, const DNSName& domain, int type, bool /* doTCP */, bool sendRDQuery, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, const ResolveContext& /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ BOOST_CHECK_EQUAL(sendRDQuery, true);
+
+ if (address != forwardedNS) {
+ return LWResult::Result::Timeout;
+ }
+
+ if (type == QType::DS || type == QType::DNSKEY) {
+ return genericDSAndDNSKEYHandler(res, domain, DNSName("."), type, keys);
+ }
+
+ if (domain == target && type == QType::A) {
+
+ setLWResult(res, 0, false, false, true);
+ addRecordToLW(res, target, QType::CNAME, cnameTarget.toString());
+ addRecordToLW(res, cnameTarget, QType::A, "192.0.2.1");
+ /* the RRSIG proves that the cnameTarget was expanded from a wildcard */
+ addRRSIG(keys, res->d_records, cnameTarget, 300, false, boost::none, DNSName("*"));
+ /* we need to add the proof that this name does not exist, so the wildcard may apply */
+ addNSECRecordToLW(DNSName("cnamd."), DNSName("cnamf."), {QType::A, QType::NSEC, QType::RRSIG}, 60, res->d_records);
+ addRRSIG(keys, res->d_records, cnameTarget, 300);
+
+ return LWResult::Result::Success;
+ }
+ return LWResult::Result::Timeout;
+ });
+
+ vector<DNSRecord> ret;
+ int res = testSR->beginResolve(target, QType(QType::A), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(testSR->getValidationState(), vState::Insecure);
+ BOOST_REQUIRE_EQUAL(ret.size(), 5U);
+ BOOST_CHECK_EQUAL(queriesCount, 5U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = testSR->beginResolve(target, QType(QType::A), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(testSR->getValidationState(), vState::Insecure);
+ BOOST_REQUIRE_EQUAL(ret.size(), 5U);
+ BOOST_CHECK_EQUAL(queriesCount, 5U);
+}
+
BOOST_AUTO_TEST_CASE(test_auth_zone_oob)
{
std::unique_ptr<SyncRes> sr;
--- /dev/null
+../views.hh
\ No newline at end of file
if (!arg) {
return SSL_TLSEXT_ERR_ALERT_WARNING;
}
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): OpenSSL's API
OpenSSLTLSIOCtx* obj = reinterpret_cast<OpenSSLTLSIOCtx*>(arg);
- size_t pos = 0;
- while (pos < inlen) {
- size_t protoLen = in[pos];
- pos++;
- if (protoLen > (inlen - pos)) {
- /* something is very wrong */
- return SSL_TLSEXT_ERR_ALERT_WARNING;
- }
+ const pdns::views::UnsignedCharView inView(in, inlen);
+ // Server preference algorithm as per RFC 7301 section 3.2
+ for (const auto& tentative : obj->d_alpnProtos) {
+ size_t pos = 0;
+ while (pos < inView.size()) {
+ size_t protoLen = inView.at(pos);
+ pos++;
+ if (protoLen > (inlen - pos)) {
+ /* something is very wrong */
+ return SSL_TLSEXT_ERR_ALERT_WARNING;
+ }
- for (const auto& tentative : obj->d_alpnProtos) {
- if (tentative.size() == protoLen && memcmp(in + pos, tentative.data(), tentative.size()) == 0) {
- *out = in + pos;
+ if (tentative.size() == protoLen && memcmp(&inView.at(pos), tentative.data(), tentative.size()) == 0) {
+ *out = &inView.at(pos);
*outlen = protoLen;
return SSL_TLSEXT_ERR_OK;
}
+ pos += protoLen;
}
- pos += protoLen;
}
return SSL_TLSEXT_ERR_NOACK;
}
if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ context.d_limitHit = true;
return false;
}
nsec3sConsidered++;
if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+ context.d_limitHit = true;
return dState::NODENIAL;
}
nsec3sConsidered++;
if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+ context.d_limitHit = true;
return dState::NODENIAL;
}
nsec3sConsidered++;
if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+ context.d_limitHit = true;
return dState::NODENIAL;
}
nsec3sConsidered++;
if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
VLOG(log, name<<": We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
// possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ context.d_limitHit = true;
break;
}
signaturesConsidered++;
for (const auto& key : keysMatchingTag) {
if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
VLOG(log, name << ": We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(signature->d_tag)<<" and algorithm "<<std::to_string(signature->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+ if (!isValid) {
+ context.d_limitHit = true;
+ }
return isValid ? vState::Secure : vState::BogusNoValidRRSIG;
}
dnskeysConsidered++;
// we need to break because we can have a partially validated set
// where the KSK signs the ZSK(s), and even if we don't
// we are going to try to get the correct EDE status (revoked, expired, ...)
+ context.d_limitHit = true;
break;
}
dnskeysConsidered++;
if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
VLOG(log, zone << ": We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
// possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ context.d_limitHit = true;
return vState::BogusNoValidDNSKEY;
}
for (const auto& key : bytag) {
if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
VLOG(log, zone << ": We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(sig->d_tag)<<" and algorithm "<<std::to_string(sig->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+ context.d_limitHit = true;
return vState::BogusNoValidDNSKEY;
}
dnskeysConsidered++;
if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
VLOG(log, zone << ": We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
// possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ context.d_limitHit = true;
return vState::BogusNoValidDNSKEY;
}
// cerr<<"validating : ";
Nsec3HashesCache d_nsec3Cache;
unsigned int d_validationsCounter{0};
unsigned int d_nsec3IterationsRemainingQuota{0};
+ bool d_limitHit{false};
};
class TooManySEC3IterationsException : public std::runtime_error
--- /dev/null
+/*
+ * 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_view>
+
+namespace pdns::views
+{
+
+class UnsignedCharView
+{
+public:
+ UnsignedCharView(const char* data_, size_t size_) :
+ view(data_, size_)
+ {
+ }
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): No unsigned char view in C++17
+ UnsignedCharView(const unsigned char* data_, size_t size_) :
+ view(reinterpret_cast<const char*>(data_), size_)
+ {
+ }
+ const unsigned char& at(std::string_view::size_type pos) const
+ {
+ return reinterpret_cast<const unsigned char&>(view.at(pos));
+ }
+
+ size_t size() const
+ {
+ return view.size();
+ }
+
+private:
+ std::string_view view;
+};
+
+}
#include <fcntl.h>
#include <iterator>
#include <linux/bpf.h>
+#include <linux/if_ether.h>
#include <linux/if_link.h>
#include <linux/if_xdp.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
+#include <linux/types.h>
+#include <linux/udp.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <netinet/in.h>
#include <unistd.h>
#include <unordered_map>
#include <vector>
-#include <linux/if_ether.h>
-#include <linux/types.h>
-#include <linux/udp.h>
#include <xdp/xsk.h>
}
};
+struct ethhdr;
struct iphdr;
struct ipv6hdr;
+struct udphdr;
class XskPacket
{
self.assertEqual(rcode, 400)
self.assertEqual(data, b'<html><body>This server implements RFC 8484 - DNS Queries over HTTP, and requires HTTP/2 in accordance with section 5.2 of the RFC.</body></html>\r\n')
+ def testDOHHTTP1NotSelectedOverH2(self):
+ """
+ DOH: Check that HTTP/1.1 is not selected over H2 when offered in the wrong order by the client
+ """
+ if self._dohLibrary == 'h2o':
+ raise unittest.SkipTest('h2o supports HTTP/1.1, this test is only relevant for nghttp2')
+ alpn = ['http/1.1', 'h2']
+ conn = self.openTLSConnection(self._dohServerPort, self._serverName, self._caCert, alpn=alpn)
+ if not hasattr(conn, 'selected_alpn_protocol'):
+ raise unittest.SkipTest('Unable to check the selected ALPN, Python version is too old to support selected_alpn_protocol')
+ self.assertEqual(conn.selected_alpn_protocol(), 'h2')
+
def testDOHInvalid(self):
"""
DOH: Invalid DNS query
response = self.sendUDPQuery(query)
self.assertEqual(len(response.options), 1)
- self.assertEqual(response.options[0].data, self._servername.encode('ascii'))
+ if dns.version.MAJOR < 2 or (dns.version.MAJOR == 2 and dns.version.MINOR < 6):
+ self.assertEqual(response.options[0].data, self._servername.encode('ascii'))
+ else:
+ self.assertEqual(response.options[0].to_text(), 'NSID ' + self._servername)
def testNSIDTCP(self):
"""
response = self.sendTCPQuery(query)
self.assertEqual(len(response.options), 1)
- self.assertEqual(response.options[0].data, self._servername.encode('ascii'))
+ if dns.version.MAJOR < 2 or (dns.version.MAJOR == 2 and dns.version.MINOR < 6):
+ self.assertEqual(response.options[0].data, self._servername.encode('ascii'))
+ else:
+ self.assertEqual(response.options[0].to_text(), 'NSID ' + self._servername)