]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/dnsdist-web.cc
Merge pull request #13387 from omoerbeek/rec-b-root-servers
[thirdparty/pdns.git] / pdns / dnsdist-web.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22
23 #include <boost/format.hpp>
24 #include <sstream>
25 #include <sys/time.h>
26 #include <sys/resource.h>
27 #include <thread>
28
29 #include "ext/json11/json11.hpp"
30 #include <yahttp/yahttp.hpp>
31
32 #include "base64.hh"
33 #include "connection-management.hh"
34 #include "dnsdist.hh"
35 #include "dnsdist-dynblocks.hh"
36 #include "dnsdist-healthchecks.hh"
37 #include "dnsdist-metrics.hh"
38 #include "dnsdist-prometheus.hh"
39 #include "dnsdist-rings.hh"
40 #include "dnsdist-web.hh"
41 #include "dolog.hh"
42 #include "gettime.hh"
43 #include "threadname.hh"
44 #include "sstuff.hh"
45
46 struct WebserverConfig
47 {
48 WebserverConfig()
49 {
50 acl.toMasks("127.0.0.1, ::1");
51 }
52
53 NetmaskGroup acl;
54 std::unique_ptr<CredentialsHolder> password;
55 std::unique_ptr<CredentialsHolder> apiKey;
56 boost::optional<std::unordered_map<std::string, std::string> > customHeaders;
57 bool apiRequiresAuthentication{true};
58 bool dashboardRequiresAuthentication{true};
59 bool statsRequireAuthentication{true};
60 };
61
62 bool g_apiReadWrite{false};
63 LockGuarded<WebserverConfig> g_webserverConfig;
64 std::string g_apiConfigDirectory;
65
66 static ConcurrentConnectionManager s_connManager(100);
67
68 std::string getWebserverConfig()
69 {
70 ostringstream out;
71
72 {
73 auto config = g_webserverConfig.lock();
74 out << "Current web server configuration:" << endl;
75 out << "ACL: " << config->acl.toString() << endl;
76 out << "Custom headers: ";
77 if (config->customHeaders) {
78 out << endl;
79 for (const auto& header : *config->customHeaders) {
80 out << " - " << header.first << ": " << header.second << endl;
81 }
82 }
83 else {
84 out << "None" << endl;
85 }
86 out << "API requires authentication: " << (config->apiRequiresAuthentication ? "yes" : "no") << endl;
87 out << "Dashboard requires authentication: " << (config->dashboardRequiresAuthentication ? "yes" : "no") << endl;
88 out << "Statistics require authentication: " << (config->statsRequireAuthentication ? "yes" : "no") << endl;
89 out << "Password: " << (config->password ? "set" : "unset") << endl;
90 out << "API key: " << (config->apiKey ? "set" : "unset") << endl;
91 }
92 out << "API writable: " << (g_apiReadWrite ? "yes" : "no") << endl;
93 out << "API configuration directory: " << g_apiConfigDirectory << endl;
94 out << "Maximum concurrent connections: " << s_connManager.getMaxConcurrentConnections() << endl;
95
96 return out.str();
97 }
98
99 class WebClientConnection
100 {
101 public:
102 WebClientConnection(const ComboAddress& client, int fd): d_client(client), d_socket(fd)
103 {
104 if (!s_connManager.registerConnection()) {
105 throw std::runtime_error("Too many concurrent web client connections");
106 }
107 }
108 WebClientConnection(WebClientConnection&& rhs): d_client(rhs.d_client), d_socket(std::move(rhs.d_socket))
109 {
110 }
111
112 WebClientConnection(const WebClientConnection&) = delete;
113 WebClientConnection& operator=(const WebClientConnection&) = delete;
114
115 ~WebClientConnection()
116 {
117 if (d_socket.getHandle() != -1) {
118 s_connManager.releaseConnection();
119 }
120 }
121
122 const Socket& getSocket() const
123 {
124 return d_socket;
125 }
126
127 const ComboAddress& getClient() const
128 {
129 return d_client;
130 }
131
132 private:
133 ComboAddress d_client;
134 Socket d_socket;
135 };
136
137 #ifndef DISABLE_PROMETHEUS
138 static MetricDefinitionStorage s_metricDefinitions;
139
140 std::map<std::string, MetricDefinition> MetricDefinitionStorage::metrics{
141 { "responses", MetricDefinition(PrometheusMetricType::counter, "Number of responses received from backends") },
142 { "servfail-responses", MetricDefinition(PrometheusMetricType::counter, "Number of SERVFAIL answers received from backends") },
143 { "queries", MetricDefinition(PrometheusMetricType::counter, "Number of received queries")},
144 { "frontend-nxdomain", MetricDefinition(PrometheusMetricType::counter, "Number of NXDomain answers sent to clients")},
145 { "frontend-servfail", MetricDefinition(PrometheusMetricType::counter, "Number of SERVFAIL answers sent to clients")},
146 { "frontend-noerror", MetricDefinition(PrometheusMetricType::counter, "Number of NoError answers sent to clients")},
147 { "acl-drops", MetricDefinition(PrometheusMetricType::counter, "Number of packets dropped because of the ACL")},
148 { "rule-drop", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because of a rule")},
149 { "rule-nxdomain", MetricDefinition(PrometheusMetricType::counter, "Number of NXDomain answers returned because of a rule")},
150 { "rule-refused", MetricDefinition(PrometheusMetricType::counter, "Number of Refused answers returned because of a rule")},
151 { "rule-servfail", MetricDefinition(PrometheusMetricType::counter, "Number of SERVFAIL answers received because of a rule")},
152 { "rule-truncated", MetricDefinition(PrometheusMetricType::counter, "Number of truncated answers returned because of a rule")},
153 { "self-answered", MetricDefinition(PrometheusMetricType::counter, "Number of self-answered responses")},
154 { "downstream-timeouts", MetricDefinition(PrometheusMetricType::counter, "Number of queries not answered in time by a backend")},
155 { "downstream-send-errors", MetricDefinition(PrometheusMetricType::counter, "Number of errors when sending a query to a backend")},
156 { "trunc-failures", MetricDefinition(PrometheusMetricType::counter, "Number of errors encountered while truncating an answer")},
157 { "no-policy", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because no server was available")},
158 { "latency0-1", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in less than 1ms")},
159 { "latency1-10", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 1-10 ms")},
160 { "latency10-50", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 10-50 ms")},
161 { "latency50-100", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 50-100 ms")},
162 { "latency100-1000", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 100-1000 ms")},
163 { "latency-slow", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in more than 1 second")},
164 { "latency-avg100", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 100 packets")},
165 { "latency-avg1000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 1000 packets")},
166 { "latency-avg10000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 10000 packets")},
167 { "latency-avg1000000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 1000000 packets")},
168 { "latency-tcp-avg100", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 100 packets received over TCP")},
169 { "latency-tcp-avg1000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000 packets received over TCP")},
170 { "latency-tcp-avg10000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 10000 packets received over TCP")},
171 { "latency-tcp-avg1000000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000000 packets received over TCP")},
172 { "latency-dot-avg100", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 100 packets received over DoT")},
173 { "latency-dot-avg1000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000 packets received over DoT")},
174 { "latency-dot-avg10000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 10000 packets received over DoT")},
175 { "latency-dot-avg1000000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000000 packets received over DoT")},
176 { "latency-doh-avg100", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 100 packets received over DoH")},
177 { "latency-doh-avg1000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000 packets received over DoH")},
178 { "latency-doh-avg10000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 10000 packets received over DoH")},
179 { "latency-doh-avg1000000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000000 packets received over DoH")},
180 { "latency-doq-avg100", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 100 packets received over DoQ")},
181 { "latency-doq-avg1000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000 packets received over DoQ")},
182 { "latency-doq-avg10000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 10000 packets received over DoQ")},
183 { "latency-doq-avg1000000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency, in microseconds, of the last 1000000 packets received over DoQ")},
184 { "uptime", MetricDefinition(PrometheusMetricType::gauge, "Uptime of the dnsdist process in seconds")},
185 { "real-memory-usage", MetricDefinition(PrometheusMetricType::gauge, "Current memory usage in bytes")},
186 { "noncompliant-queries", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped as non-compliant")},
187 { "noncompliant-responses", MetricDefinition(PrometheusMetricType::counter, "Number of answers from a backend dropped as non-compliant")},
188 { "rdqueries", MetricDefinition(PrometheusMetricType::counter, "Number of received queries with the recursion desired bit set")},
189 { "empty-queries", MetricDefinition(PrometheusMetricType::counter, "Number of empty queries received from clients")},
190 { "cache-hits", MetricDefinition(PrometheusMetricType::counter, "Number of times an answer was retrieved from cache")},
191 { "cache-misses", MetricDefinition(PrometheusMetricType::counter, "Number of times an answer not found in the cache")},
192 { "cpu-iowait", MetricDefinition(PrometheusMetricType::counter, "Time waiting for I/O to complete by the whole system, in units of USER_HZ")},
193 { "cpu-user-msec", MetricDefinition(PrometheusMetricType::counter, "Milliseconds spent by dnsdist in the user state")},
194 { "cpu-steal", MetricDefinition(PrometheusMetricType::counter, "Stolen time, which is the time spent by the whole system in other operating systems when running in a virtualized environment, in units of USER_HZ")},
195 { "cpu-sys-msec", MetricDefinition(PrometheusMetricType::counter, "Milliseconds spent by dnsdist in the system state")},
196 { "fd-usage", MetricDefinition(PrometheusMetricType::gauge, "Number of currently used file descriptors")},
197 { "dyn-blocked", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because of a dynamic block")},
198 { "dyn-block-nmg-size", MetricDefinition(PrometheusMetricType::gauge, "Number of dynamic blocks entries") },
199 { "security-status", MetricDefinition(PrometheusMetricType::gauge, "Security status of this software. 0=unknown, 1=OK, 2=upgrade recommended, 3=upgrade mandatory") },
200 { "doh-query-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of DoH queries dropped because the internal pipe used to distribute queries was full") },
201 { "doh-response-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of DoH responses dropped because the internal pipe used to distribute responses was full") },
202 { "outgoing-doh-query-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of outgoing DoH queries dropped because the internal pipe used to distribute queries was full") },
203 { "tcp-query-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of TCP queries dropped because the internal pipe used to distribute queries was full") },
204 { "tcp-cross-protocol-query-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of TCP cross-protocol queries dropped because the internal pipe used to distribute queries was full") },
205 { "tcp-cross-protocol-response-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of TCP cross-protocol responses dropped because the internal pipe used to distribute queries was full") },
206 { "udp-in-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp InErrors") },
207 { "udp-noport-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp NoPorts") },
208 { "udp-recvbuf-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp RcvbufErrors") },
209 { "udp-sndbuf-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp SndbufErrors") },
210 { "udp-in-csum-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp InCsumErrors") },
211 { "udp6-in-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp6 Udp6InErrors") },
212 { "udp6-recvbuf-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp6 Udp6RcvbufErrors") },
213 { "udp6-sndbuf-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp6 Udp6SndbufErrors") },
214 { "udp6-noport-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp6 Udp6NoPorts") },
215 { "udp6-in-csum-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp6 Udp6InCsumErrors") },
216 { "tcp-listen-overflows", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/netstat ListenOverflows") },
217 { "proxy-protocol-invalid", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because of an invalid Proxy Protocol header") },
218 };
219 #endif /* DISABLE_PROMETHEUS */
220
221 bool addMetricDefinition(const dnsdist::prometheus::PrometheusMetricDefinition& def) {
222 #ifndef DISABLE_PROMETHEUS
223 return MetricDefinitionStorage::addMetricDefinition(def);
224 #else
225 return true;
226 #endif /* DISABLE_PROMETHEUS */
227 }
228
229 #ifndef DISABLE_WEB_CONFIG
230 static bool apiWriteConfigFile(const string& filebasename, const string& content)
231 {
232 if (!g_apiReadWrite) {
233 warnlog("Not writing content to %s since the API is read-only", filebasename);
234 return false;
235 }
236
237 if (g_apiConfigDirectory.empty()) {
238 vinfolog("Not writing content to %s since the API configuration directory is not set", filebasename);
239 return false;
240 }
241
242 string filename = g_apiConfigDirectory + "/" + filebasename + ".conf";
243 ofstream ofconf(filename.c_str());
244 if (!ofconf) {
245 errlog("Could not open configuration fragment file '%s' for writing: %s", filename, stringerror());
246 return false;
247 }
248 ofconf << "-- Generated by the REST API, DO NOT EDIT" << endl;
249 ofconf << content << endl;
250 ofconf.close();
251 return true;
252 }
253
254 static void apiSaveACL(const NetmaskGroup& nmg)
255 {
256 vector<string> vec;
257 nmg.toStringVector(&vec);
258
259 string acl;
260 for(const auto& s : vec) {
261 if (!acl.empty()) {
262 acl += ", ";
263 }
264 acl += "\"" + s + "\"";
265 }
266
267 string content = "setACL({" + acl + "})";
268 apiWriteConfigFile("acl", content);
269 }
270 #endif /* DISABLE_WEB_CONFIG */
271
272 static bool checkAPIKey(const YaHTTP::Request& req, const std::unique_ptr<CredentialsHolder>& apiKey)
273 {
274 if (!apiKey) {
275 return false;
276 }
277
278 const auto header = req.headers.find("x-api-key");
279 if (header != req.headers.end()) {
280 return apiKey->matches(header->second);
281 }
282
283 return false;
284 }
285
286 static bool checkWebPassword(const YaHTTP::Request& req, const std::unique_ptr<CredentialsHolder>& password, bool dashboardRequiresAuthentication)
287 {
288 if (!dashboardRequiresAuthentication) {
289 return true;
290 }
291
292 static const char basicStr[] = "basic ";
293
294 const auto header = req.headers.find("authorization");
295
296 if (header != req.headers.end() && toLower(header->second).find(basicStr) == 0) {
297 string cookie = header->second.substr(sizeof(basicStr) - 1);
298
299 string plain;
300 B64Decode(cookie, plain);
301
302 vector<string> cparts;
303 stringtok(cparts, plain, ":");
304
305 if (cparts.size() == 2) {
306 if (password) {
307 return password->matches(cparts.at(1));
308 }
309 return true;
310 }
311 }
312
313 return false;
314 }
315
316 static bool isAnAPIRequest(const YaHTTP::Request& req)
317 {
318 return req.url.path.find("/api/") == 0;
319 }
320
321 static bool isAnAPIRequestAllowedWithWebAuth(const YaHTTP::Request& req)
322 {
323 return req.url.path == "/api/v1/servers/localhost";
324 }
325
326 static bool isAStatsRequest(const YaHTTP::Request& req)
327 {
328 return req.url.path == "/jsonstat" || req.url.path == "/metrics";
329 }
330
331 static bool handleAuthorization(const YaHTTP::Request& req)
332 {
333 auto config = g_webserverConfig.lock();
334
335 if (isAStatsRequest(req)) {
336 if (config->statsRequireAuthentication) {
337 /* Access to the stats is allowed for both API and Web users */
338 return checkAPIKey(req, config->apiKey) || checkWebPassword(req, config->password, config->dashboardRequiresAuthentication);
339 }
340 return true;
341 }
342
343 if (isAnAPIRequest(req)) {
344 /* Access to the API requires a valid API key */
345 if (!config->apiRequiresAuthentication || checkAPIKey(req, config->apiKey)) {
346 return true;
347 }
348
349 return isAnAPIRequestAllowedWithWebAuth(req) && checkWebPassword(req, config->password, config->dashboardRequiresAuthentication);
350 }
351
352 return checkWebPassword(req, config->password, config->dashboardRequiresAuthentication);
353 }
354
355 static bool isMethodAllowed(const YaHTTP::Request& req)
356 {
357 if (req.method == "GET") {
358 return true;
359 }
360 if (req.method == "PUT" && g_apiReadWrite) {
361 if (req.url.path == "/api/v1/servers/localhost/config/allow-from") {
362 return true;
363 }
364 }
365 #ifndef DISABLE_WEB_CACHE_MANAGEMENT
366 if (req.method == "DELETE") {
367 if (req.url.path == "/api/v1/cache") {
368 return true;
369 }
370 }
371 #endif /* DISABLE_WEB_CACHE_MANAGEMENT */
372 return false;
373 }
374
375 static bool isClientAllowedByACL(const ComboAddress& remote)
376 {
377 return g_webserverConfig.lock()->acl.match(remote);
378 }
379
380 static void handleCORS(const YaHTTP::Request& req, YaHTTP::Response& resp)
381 {
382 const auto origin = req.headers.find("Origin");
383 if (origin != req.headers.end()) {
384 if (req.method == "OPTIONS") {
385 /* Pre-flight request */
386 if (g_apiReadWrite) {
387 resp.headers["Access-Control-Allow-Methods"] = "GET, PUT";
388 }
389 else {
390 resp.headers["Access-Control-Allow-Methods"] = "GET";
391 }
392 resp.headers["Access-Control-Allow-Headers"] = "Authorization, X-API-Key";
393 }
394
395 resp.headers["Access-Control-Allow-Origin"] = origin->second;
396
397 if (isAStatsRequest(req) || isAnAPIRequestAllowedWithWebAuth(req)) {
398 resp.headers["Access-Control-Allow-Credentials"] = "true";
399 }
400 }
401 }
402
403 static void addSecurityHeaders(YaHTTP::Response& resp, const boost::optional<std::unordered_map<std::string, std::string> >& customHeaders)
404 {
405 static const std::vector<std::pair<std::string, std::string> > headers = {
406 { "X-Content-Type-Options", "nosniff" },
407 { "X-Frame-Options", "deny" },
408 { "X-Permitted-Cross-Domain-Policies", "none" },
409 { "X-XSS-Protection", "1; mode=block" },
410 { "Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'" },
411 };
412
413 for (const auto& h : headers) {
414 if (customHeaders) {
415 const auto& custom = customHeaders->find(h.first);
416 if (custom != customHeaders->end()) {
417 continue;
418 }
419 }
420 resp.headers[h.first] = h.second;
421 }
422 }
423
424 static void addCustomHeaders(YaHTTP::Response& resp, const boost::optional<std::unordered_map<std::string, std::string> >& customHeaders)
425 {
426 if (!customHeaders)
427 return;
428
429 for (const auto& c : *customHeaders) {
430 if (!c.second.empty()) {
431 resp.headers[c.first] = c.second;
432 }
433 }
434 }
435
436 template<typename T>
437 static json11::Json::array someResponseRulesToJson(GlobalStateHolder<vector<T>>* someResponseRules)
438 {
439 using namespace json11;
440 Json::array responseRules;
441 int num=0;
442 auto localResponseRules = someResponseRules->getLocal();
443 responseRules.reserve(localResponseRules->size());
444 for (const auto& a : *localResponseRules) {
445 responseRules.push_back(Json::object{
446 {"id", num++},
447 {"creationOrder", (double)a.d_creationOrder},
448 {"uuid", boost::uuids::to_string(a.d_id)},
449 {"name", a.d_name},
450 {"matches", (double)a.d_rule->d_matches},
451 {"rule", a.d_rule->toString()},
452 {"action", a.d_action->toString()},
453 });
454 }
455 return responseRules;
456 }
457
458 #ifndef DISABLE_PROMETHEUS
459 template<typename T>
460 static void addRulesToPrometheusOutput(std::ostringstream& output, GlobalStateHolder<vector<T> >& rules)
461 {
462 auto localRules = rules.getLocal();
463 for (const auto& entry : *localRules) {
464 std::string id = !entry.d_name.empty() ? entry.d_name : boost::uuids::to_string(entry.d_id);
465 output << "dnsdist_rule_hits{id=\"" << id << "\"} " << entry.d_rule->d_matches << "\n";
466 }
467 }
468
469 static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
470 {
471 handleCORS(req, resp);
472 resp.status = 200;
473
474 std::ostringstream output;
475 static const std::set<std::string> metricBlacklist = { "special-memory-usage", "latency-count", "latency-sum" };
476 {
477 auto entries = dnsdist::metrics::g_stats.entries.read_lock();
478 for (const auto& entry : *entries) {
479 const auto& metricName = entry.d_name;
480
481 if (metricBlacklist.count(metricName) != 0) {
482 continue;
483 }
484
485 MetricDefinition metricDetails;
486 if (!s_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
487 vinfolog("Do not have metric details for %s", metricName);
488 continue;
489 }
490
491 const std::string prometheusTypeName = s_metricDefinitions.getPrometheusStringMetricType(metricDetails.prometheusType);
492 if (prometheusTypeName.empty()) {
493 vinfolog("Unknown Prometheus type for %s", metricName);
494 continue;
495 }
496
497 // Prometheus suggest using '_' instead of '-'
498 std::string prometheusMetricName;
499 if (metricDetails.customName.empty()) {
500 prometheusMetricName = "dnsdist_" + boost::replace_all_copy(metricName, "-", "_");
501 }
502 else {
503 prometheusMetricName = metricDetails.customName;
504 }
505
506 // for these we have the help and types encoded in the sources
507 // but we need to be careful about labels in custom metrics
508 std::string helpName = prometheusMetricName.substr(0, prometheusMetricName.find('{'));
509 output << "# HELP " << helpName << " " << metricDetails.description << "\n";
510 output << "# TYPE " << helpName << " " << prometheusTypeName << "\n";
511 output << prometheusMetricName << " ";
512
513 if (const auto& val = std::get_if<pdns::stat_t*>(&entry.d_value)) {
514 output << (*val)->load();
515 }
516 else if (const auto& adval = std::get_if<pdns::stat_t_trait<double>*>(&entry.d_value)) {
517 output << (*adval)->load();
518 }
519 else if (const auto& dval = std::get_if<double*>(&entry.d_value)) {
520 output << **dval;
521 }
522 else if (const auto& func = std::get_if<dnsdist::metrics::Stats::statfunction_t>(&entry.d_value)) {
523 output << (*func)(entry.d_name);
524 }
525
526 output << "\n";
527 }
528 }
529
530 // Latency histogram buckets
531 output << "# HELP dnsdist_latency Histogram of responses by latency (in milliseconds)\n";
532 output << "# TYPE dnsdist_latency histogram\n";
533 uint64_t latency_amounts = dnsdist::metrics::g_stats.latency0_1;
534 output << "dnsdist_latency_bucket{le=\"1\"} " << latency_amounts << "\n";
535 latency_amounts += dnsdist::metrics::g_stats.latency1_10;
536 output << "dnsdist_latency_bucket{le=\"10\"} " << latency_amounts << "\n";
537 latency_amounts += dnsdist::metrics::g_stats.latency10_50;
538 output << "dnsdist_latency_bucket{le=\"50\"} " << latency_amounts << "\n";
539 latency_amounts += dnsdist::metrics::g_stats.latency50_100;
540 output << "dnsdist_latency_bucket{le=\"100\"} " << latency_amounts << "\n";
541 latency_amounts += dnsdist::metrics::g_stats.latency100_1000;
542 output << "dnsdist_latency_bucket{le=\"1000\"} " << latency_amounts << "\n";
543 latency_amounts += dnsdist::metrics::g_stats.latencySlow; // Should be the same as latency_count
544 output << "dnsdist_latency_bucket{le=\"+Inf\"} " << latency_amounts << "\n";
545 output << "dnsdist_latency_sum " << dnsdist::metrics::g_stats.latencySum << "\n";
546 output << "dnsdist_latency_count " << dnsdist::metrics::g_stats.latencyCount << "\n";
547
548 auto states = g_dstates.getLocal();
549 const string statesbase = "dnsdist_server_";
550
551 output << "# HELP " << statesbase << "status " << "Whether this backend is up (1) or down (0)" << "\n";
552 output << "# TYPE " << statesbase << "status " << "gauge" << "\n";
553 output << "# HELP " << statesbase << "queries " << "Amount of queries relayed to server" << "\n";
554 output << "# TYPE " << statesbase << "queries " << "counter" << "\n";
555 output << "# HELP " << statesbase << "responses " << "Amount of responses received from this server" << "\n";
556 output << "# TYPE " << statesbase << "responses " << "counter" << "\n";
557 output << "# HELP " << statesbase << "noncompliantresponses " << "Amount of non-compliant responses received from this server" << "\n";
558 output << "# TYPE " << statesbase << "noncompliantresponses " << "counter" << "\n";
559 output << "# HELP " << statesbase << "drops " << "Amount of queries not answered by server" << "\n";
560 output << "# TYPE " << statesbase << "drops " << "counter" << "\n";
561 output << "# HELP " << statesbase << "latency " << "Server's latency when answering questions in milliseconds" << "\n";
562 output << "# TYPE " << statesbase << "latency " << "gauge" << "\n";
563 output << "# HELP " << statesbase << "senderrors " << "Total number of OS send errors while relaying queries" << "\n";
564 output << "# TYPE " << statesbase << "senderrors " << "counter" << "\n";
565 output << "# HELP " << statesbase << "outstanding " << "Current number of queries that are waiting for a backend response" << "\n";
566 output << "# TYPE " << statesbase << "outstanding " << "gauge" << "\n";
567 output << "# HELP " << statesbase << "order " << "The order in which this server is picked" << "\n";
568 output << "# TYPE " << statesbase << "order " << "gauge" << "\n";
569 output << "# HELP " << statesbase << "weight " << "The weight within the order in which this server is picked" << "\n";
570 output << "# TYPE " << statesbase << "weight " << "gauge" << "\n";
571 output << "# HELP " << statesbase << "tcpdiedsendingquery " << "The number of TCP I/O errors while sending the query" << "\n";
572 output << "# TYPE " << statesbase << "tcpdiedsendingquery " << "counter" << "\n";
573 output << "# HELP " << statesbase << "tcpdiedreadingresponse " << "The number of TCP I/O errors while reading the response" << "\n";
574 output << "# TYPE " << statesbase << "tcpdiedreadingresponse " << "counter" << "\n";
575 output << "# HELP " << statesbase << "tcpgaveup " << "The number of TCP connections failing after too many attempts" << "\n";
576 output << "# TYPE " << statesbase << "tcpgaveup " << "counter" << "\n";
577 output << "# HELP " << statesbase << "tcpconnecttimeouts " << "The number of TCP connect timeouts" << "\n";
578 output << "# TYPE " << statesbase << "tcpconnecttimeouts " << "counter" << "\n";
579 output << "# HELP " << statesbase << "tcpreadtimeouts " << "The number of TCP read timeouts" << "\n";
580 output << "# TYPE " << statesbase << "tcpreadtimeouts " << "counter" << "\n";
581 output << "# HELP " << statesbase << "tcpwritetimeouts " << "The number of TCP write timeouts" << "\n";
582 output << "# TYPE " << statesbase << "tcpwritetimeouts " << "counter" << "\n";
583 output << "# HELP " << statesbase << "tcpcurrentconnections " << "The number of current TCP connections" << "\n";
584 output << "# TYPE " << statesbase << "tcpcurrentconnections " << "gauge" << "\n";
585 output << "# HELP " << statesbase << "tcpmaxconcurrentconnections " << "The maximum number of concurrent TCP connections" << "\n";
586 output << "# TYPE " << statesbase << "tcpmaxconcurrentconnections " << "counter" << "\n";
587 output << "# HELP " << statesbase << "tcptoomanyconcurrentconnections " << "Number of times we had to enforce the maximum number of concurrent TCP connections" << "\n";
588 output << "# TYPE " << statesbase << "tcptoomanyconcurrentconnections " << "counter" << "\n";
589 output << "# HELP " << statesbase << "tcpnewconnections " << "The number of established TCP connections in total" << "\n";
590 output << "# TYPE " << statesbase << "tcpnewconnections " << "counter" << "\n";
591 output << "# HELP " << statesbase << "tcpreusedconnections " << "The number of times a TCP connection has been reused" << "\n";
592 output << "# TYPE " << statesbase << "tcpreusedconnections " << "counter" << "\n";
593 output << "# HELP " << statesbase << "tcpavgqueriesperconn " << "The average number of queries per TCP connection" << "\n";
594 output << "# TYPE " << statesbase << "tcpavgqueriesperconn " << "gauge" << "\n";
595 output << "# HELP " << statesbase << "tcpavgconnduration " << "The average duration of a TCP connection (ms)" << "\n";
596 output << "# TYPE " << statesbase << "tcpavgconnduration " << "gauge" << "\n";
597 output << "# HELP " << statesbase << "tlsresumptions " << "The number of times a TLS session has been resumed" << "\n";
598 output << "# TYPE " << statesbase << "tlsresumptions " << "counter" << "\n";
599 output << "# HELP " << statesbase << "tcplatency " << "Server's latency when answering TCP questions in milliseconds" << "\n";
600 output << "# TYPE " << statesbase << "tcplatency " << "gauge" << "\n";
601 output << "# HELP " << statesbase << "healthcheckfailures " << "Number of health check attempts that failed (total)" << "\n";
602 output << "# TYPE " << statesbase << "healthcheckfailures " << "counter" << "\n";
603 output << "# HELP " << statesbase << "healthcheckfailuresparsing " << "Number of health check attempts where the response could not be parsed" << "\n";
604 output << "# TYPE " << statesbase << "healthcheckfailuresparsing " << "counter" << "\n";
605 output << "# HELP " << statesbase << "healthcheckfailurestimeout " << "Number of health check attempts where the response did not arrive in time" << "\n";
606 output << "# TYPE " << statesbase << "healthcheckfailurestimeout " << "counter" << "\n";
607 output << "# HELP " << statesbase << "healthcheckfailuresnetwork " << "Number of health check attempts that experienced a network issue" << "\n";
608 output << "# TYPE " << statesbase << "healthcheckfailuresnetwork " << "counter" << "\n";
609 output << "# HELP " << statesbase << "healthcheckfailuresmismatch " << "Number of health check attempts where the response did not match the query" << "\n";
610 output << "# TYPE " << statesbase << "healthcheckfailuresmismatch " << "counter" << "\n";
611 output << "# HELP " << statesbase << "healthcheckfailuresinvalid " << "Number of health check attempts where the DNS response was invalid" << "\n";
612 output << "# TYPE " << statesbase << "healthcheckfailuresinvalid " << "counter" << "\n";
613
614 for (const auto& state : *states) {
615 string serverName;
616
617 if (state->getName().empty()) {
618 serverName = state->d_config.remote.toStringWithPort();
619 }
620 else {
621 serverName = state->getName();
622 }
623
624 boost::replace_all(serverName, ".", "_");
625
626 const std::string label = boost::str(boost::format("{server=\"%1%\",address=\"%2%\"}")
627 % serverName % state->d_config.remote.toStringWithPort());
628
629 output << statesbase << "status" << label << " " << (state->isUp() ? "1" : "0") << "\n";
630 output << statesbase << "queries" << label << " " << state->queries.load() << "\n";
631 output << statesbase << "responses" << label << " " << state->responses.load() << "\n";
632 output << statesbase << "noncompliantresponses" << label << " " << state->nonCompliantResponses.load() << "\n";
633 output << statesbase << "drops" << label << " " << state->reuseds.load() << "\n";
634 if (state->isUp()) {
635 output << statesbase << "latency" << label << " " << state->latencyUsec/1000.0 << "\n";
636 output << statesbase << "tcplatency" << label << " " << state->latencyUsecTCP/1000.0 << "\n";
637 }
638 output << statesbase << "senderrors" << label << " " << state->sendErrors.load() << "\n";
639 output << statesbase << "outstanding" << label << " " << state->outstanding.load() << "\n";
640 output << statesbase << "order" << label << " " << state->d_config.order << "\n";
641 output << statesbase << "weight" << label << " " << state->d_config.d_weight << "\n";
642 output << statesbase << "tcpdiedsendingquery" << label << " " << state->tcpDiedSendingQuery << "\n";
643 output << statesbase << "tcpdiedreadingresponse" << label << " " << state->tcpDiedReadingResponse << "\n";
644 output << statesbase << "tcpgaveup" << label << " " << state->tcpGaveUp << "\n";
645 output << statesbase << "tcpreadtimeouts" << label << " " << state->tcpReadTimeouts << "\n";
646 output << statesbase << "tcpwritetimeouts" << label << " " << state->tcpWriteTimeouts << "\n";
647 output << statesbase << "tcpconnecttimeouts" << label << " " << state->tcpConnectTimeouts << "\n";
648 output << statesbase << "tcpcurrentconnections" << label << " " << state->tcpCurrentConnections << "\n";
649 output << statesbase << "tcpmaxconcurrentconnections" << label << " " << state->tcpMaxConcurrentConnections << "\n";
650 output << statesbase << "tcptoomanyconcurrentconnections" << label << " " << state->tcpTooManyConcurrentConnections << "\n";
651 output << statesbase << "tcpnewconnections" << label << " " << state->tcpNewConnections << "\n";
652 output << statesbase << "tcpreusedconnections" << label << " " << state->tcpReusedConnections << "\n";
653 output << statesbase << "tcpavgqueriesperconn" << label << " " << state->tcpAvgQueriesPerConnection << "\n";
654 output << statesbase << "tcpavgconnduration" << label << " " << state->tcpAvgConnectionDuration << "\n";
655 output << statesbase << "tlsresumptions" << label << " " << state->tlsResumptions << "\n";
656 output << statesbase << "healthcheckfailures" << label << " " << state->d_healthCheckMetrics.d_failures << "\n";
657 output << statesbase << "healthcheckfailuresparsing" << label << " " << state->d_healthCheckMetrics.d_parseErrors << "\n";
658 output << statesbase << "healthcheckfailurestimeout" << label << " " << state->d_healthCheckMetrics.d_timeOuts << "\n";
659 output << statesbase << "healthcheckfailuresnetwork" << label << " " << state->d_healthCheckMetrics.d_networkErrors << "\n";
660 output << statesbase << "healthcheckfailuresmismatch" << label << " " << state->d_healthCheckMetrics.d_mismatchErrors << "\n";
661 output << statesbase << "healthcheckfailuresinvalid" << label << " " << state->d_healthCheckMetrics.d_invalidResponseErrors << "\n";
662 }
663
664 const string frontsbase = "dnsdist_frontend_";
665 output << "# HELP " << frontsbase << "queries " << "Amount of queries received by this frontend" << "\n";
666 output << "# TYPE " << frontsbase << "queries " << "counter" << "\n";
667 output << "# HELP " << frontsbase << "noncompliantqueries " << "Amount of non-compliant queries received by this frontend" << "\n";
668 output << "# TYPE " << frontsbase << "noncompliantqueries " << "counter" << "\n";
669 output << "# HELP " << frontsbase << "responses " << "Amount of responses sent by this frontend" << "\n";
670 output << "# TYPE " << frontsbase << "responses " << "counter" << "\n";
671 output << "# HELP " << frontsbase << "tcpdiedreadingquery " << "Amount of TCP connections terminated while reading the query from the client" << "\n";
672 output << "# TYPE " << frontsbase << "tcpdiedreadingquery " << "counter" << "\n";
673 output << "# HELP " << frontsbase << "tcpdiedsendingresponse " << "Amount of TCP connections terminated while sending a response to the client" << "\n";
674 output << "# TYPE " << frontsbase << "tcpdiedsendingresponse " << "counter" << "\n";
675 output << "# HELP " << frontsbase << "tcpgaveup " << "Amount of TCP connections terminated after too many attempts to get a connection to the backend" << "\n";
676 output << "# TYPE " << frontsbase << "tcpgaveup " << "counter" << "\n";
677 output << "# HELP " << frontsbase << "tcpclienttimeouts " << "Amount of TCP connections terminated by a timeout while reading from the client" << "\n";
678 output << "# TYPE " << frontsbase << "tcpclienttimeouts " << "counter" << "\n";
679 output << "# HELP " << frontsbase << "tcpdownstreamtimeouts " << "Amount of TCP connections terminated by a timeout while reading from the backend" << "\n";
680 output << "# TYPE " << frontsbase << "tcpdownstreamtimeouts " << "counter" << "\n";
681 output << "# HELP " << frontsbase << "tcpcurrentconnections " << "Amount of current incoming TCP connections from clients" << "\n";
682 output << "# TYPE " << frontsbase << "tcpcurrentconnections " << "gauge" << "\n";
683 output << "# HELP " << frontsbase << "tcpmaxconcurrentconnections " << "Maximum number of concurrent incoming TCP connections from clients" << "\n";
684 output << "# TYPE " << frontsbase << "tcpmaxconcurrentconnections " << "counter" << "\n";
685 output << "# HELP " << frontsbase << "tcpavgqueriesperconnection " << "The average number of queries per TCP connection" << "\n";
686 output << "# TYPE " << frontsbase << "tcpavgqueriesperconnection " << "gauge" << "\n";
687 output << "# HELP " << frontsbase << "tcpavgconnectionduration " << "The average duration of a TCP connection (ms)" << "\n";
688 output << "# TYPE " << frontsbase << "tcpavgconnectionduration " << "gauge" << "\n";
689 output << "# HELP " << frontsbase << "tlsqueries " << "Number of queries received by dnsdist over TLS, by TLS version" << "\n";
690 output << "# TYPE " << frontsbase << "tlsqueries " << "counter" << "\n";
691 output << "# HELP " << frontsbase << "tlsnewsessions " << "Amount of new TLS sessions negotiated" << "\n";
692 output << "# TYPE " << frontsbase << "tlsnewsessions " << "counter" << "\n";
693 output << "# HELP " << frontsbase << "tlsresumptions " << "Amount of TLS sessions resumed" << "\n";
694 output << "# TYPE " << frontsbase << "tlsresumptions " << "counter" << "\n";
695 output << "# HELP " << frontsbase << "tlsunknownticketkeys " << "Amount of attempts to resume TLS session from an unknown key (possibly expired)" << "\n";
696 output << "# TYPE " << frontsbase << "tlsunknownticketkeys " << "counter" << "\n";
697 output << "# HELP " << frontsbase << "tlsinactiveticketkeys " << "Amount of TLS sessions resumed from an inactive key" << "\n";
698 output << "# TYPE " << frontsbase << "tlsinactiveticketkeys " << "counter" << "\n";
699 output << "# HELP " << frontsbase << "tlshandshakefailures " << "Amount of TLS handshake failures" << "\n";
700 output << "# TYPE " << frontsbase << "tlshandshakefailures " << "counter" << "\n";
701
702 std::map<std::string,uint64_t> frontendDuplicates;
703 for (const auto& front : g_frontends) {
704 if (front->udpFD == -1 && front->tcpFD == -1)
705 continue;
706
707 const string frontName = front->local.toStringWithPort();
708 const string proto = front->getType();
709 const string fullName = frontName + "_" + proto;
710 uint64_t threadNumber = 0;
711 auto dupPair = frontendDuplicates.emplace(fullName, 1);
712 if (!dupPair.second) {
713 threadNumber = dupPair.first->second;
714 ++(dupPair.first->second);
715 }
716 const std::string label = boost::str(boost::format("{frontend=\"%1%\",proto=\"%2%\",thread=\"%3%\"} ")
717 % frontName % proto % threadNumber);
718
719 output << frontsbase << "queries" << label << front->queries.load() << "\n";
720 output << frontsbase << "noncompliantqueries" << label << front->nonCompliantQueries.load() << "\n";
721 output << frontsbase << "responses" << label << front->responses.load() << "\n";
722 if (front->isTCP()) {
723 output << frontsbase << "tcpdiedreadingquery" << label << front->tcpDiedReadingQuery.load() << "\n";
724 output << frontsbase << "tcpdiedsendingresponse" << label << front->tcpDiedSendingResponse.load() << "\n";
725 output << frontsbase << "tcpgaveup" << label << front->tcpGaveUp.load() << "\n";
726 output << frontsbase << "tcpclienttimeouts" << label << front->tcpClientTimeouts.load() << "\n";
727 output << frontsbase << "tcpdownstreamtimeouts" << label << front->tcpDownstreamTimeouts.load() << "\n";
728 output << frontsbase << "tcpcurrentconnections" << label << front->tcpCurrentConnections.load() << "\n";
729 output << frontsbase << "tcpmaxconcurrentconnections" << label << front->tcpMaxConcurrentConnections.load() << "\n";
730 output << frontsbase << "tcpavgqueriesperconnection" << label << front->tcpAvgQueriesPerConnection.load() << "\n";
731 output << frontsbase << "tcpavgconnectionduration" << label << front->tcpAvgConnectionDuration.load() << "\n";
732 if (front->hasTLS()) {
733 output << frontsbase << "tlsnewsessions" << label << front->tlsNewSessions.load() << "\n";
734 output << frontsbase << "tlsresumptions" << label << front->tlsResumptions.load() << "\n";
735 output << frontsbase << "tlsunknownticketkeys" << label << front->tlsUnknownTicketKey.load() << "\n";
736 output << frontsbase << "tlsinactiveticketkeys" << label << front->tlsInactiveTicketKey.load() << "\n";
737
738 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls10\"} " << front->tls10queries.load() << "\n";
739 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls11\"} " << front->tls11queries.load() << "\n";
740 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls12\"} " << front->tls12queries.load() << "\n";
741 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls13\"} " << front->tls13queries.load() << "\n";
742 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"unknown\"} " << front->tlsUnknownqueries.load() << "\n";
743
744 const TLSErrorCounters* errorCounters = nullptr;
745 if (front->tlsFrontend != nullptr) {
746 errorCounters = &front->tlsFrontend->d_tlsCounters;
747 }
748 else if (front->dohFrontend != nullptr) {
749 errorCounters = &front->dohFrontend->d_tlsContext.d_tlsCounters;
750 }
751
752 if (errorCounters != nullptr) {
753 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"dhKeyTooSmall\"} " << errorCounters->d_dhKeyTooSmall << "\n";
754 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"inappropriateFallBack\"} " << errorCounters->d_inappropriateFallBack << "\n";
755 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"noSharedCipher\"} " << errorCounters->d_noSharedCipher << "\n";
756 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownCipherType\"} " << errorCounters->d_unknownCipherType << "\n";
757 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownKeyExchangeType\"} " << errorCounters->d_unknownKeyExchangeType << "\n";
758 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownProtocol\"} " << errorCounters->d_unknownProtocol << "\n";
759 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedEC\"} " << errorCounters->d_unsupportedEC << "\n";
760 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedProtocol\"} " << errorCounters->d_unsupportedProtocol << "\n";
761 }
762 }
763 }
764 }
765
766 output << "# HELP " << frontsbase << "http_connects " << "Number of DoH TCP connections established to this frontend" << "\n";
767 output << "# TYPE " << frontsbase << "http_connects " << "counter" << "\n";
768
769 output << "# HELP " << frontsbase << "doh_http_method_queries " << "Number of DoH queries received by dnsdist, by HTTP method" << "\n";
770 output << "# TYPE " << frontsbase << "doh_http_method_queries " << "counter" << "\n";
771
772 output << "# HELP " << frontsbase << "doh_http_version_queries " << "Number of DoH queries received by dnsdist, by HTTP version" << "\n";
773 output << "# TYPE " << frontsbase << "doh_http_version_queries " << "counter" << "\n";
774
775 output << "# HELP " << frontsbase << "doh_bad_requests " << "Number of requests that could not be converted to a DNS query" << "\n";
776 output << "# TYPE " << frontsbase << "doh_bad_requests " << "counter" << "\n";
777
778 output << "# HELP " << frontsbase << "doh_responses " << "Number of responses sent, by type" << "\n";
779 output << "# TYPE " << frontsbase << "doh_responses " << "counter" << "\n";
780
781 output << "# HELP " << frontsbase << "doh_version_status_responses " << "Number of requests that could not be converted to a DNS query" << "\n";
782 output << "# TYPE " << frontsbase << "doh_version_status_responses " << "counter" << "\n";
783
784 #ifdef HAVE_DNS_OVER_HTTPS
785 std::map<std::string,uint64_t> dohFrontendDuplicates;
786 for(const auto& doh : g_dohlocals) {
787 const string frontName = doh->d_tlsContext.d_addr.toStringWithPort();
788 uint64_t threadNumber = 0;
789 auto dupPair = frontendDuplicates.emplace(frontName, 1);
790 if (!dupPair.second) {
791 threadNumber = dupPair.first->second;
792 ++(dupPair.first->second);
793 }
794 const std::string addrlabel = boost::str(boost::format("frontend=\"%1%\",thread=\"%2%\"") % frontName % threadNumber);
795 const std::string label = "{" + addrlabel + "} ";
796
797 output << frontsbase << "http_connects" << label << doh->d_httpconnects << "\n";
798 output << frontsbase << "doh_http_method_queries{method=\"get\"," << addrlabel << "} " << doh->d_getqueries << "\n";
799 output << frontsbase << "doh_http_method_queries{method=\"post\"," << addrlabel << "} " << doh->d_postqueries << "\n";
800
801 output << frontsbase << "doh_http_version_queries{version=\"1\"," << addrlabel << "} " << doh->d_http1Stats.d_nbQueries << "\n";
802 output << frontsbase << "doh_http_version_queries{version=\"2\"," << addrlabel << "} " << doh->d_http2Stats.d_nbQueries << "\n";
803
804 output << frontsbase << "doh_bad_requests{" << addrlabel << "} " << doh->d_badrequests << "\n";
805
806 output << frontsbase << "doh_responses{type=\"error\"," << addrlabel << "} " << doh->d_errorresponses << "\n";
807 output << frontsbase << "doh_responses{type=\"redirect\"," << addrlabel << "} " << doh->d_redirectresponses << "\n";
808 output << frontsbase << "doh_responses{type=\"valid\"," << addrlabel << "} " << doh->d_validresponses << "\n";
809
810 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"200\"," << addrlabel << "} " << doh->d_http1Stats.d_nb200Responses << "\n";
811 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"400\"," << addrlabel << "} " << doh->d_http1Stats.d_nb400Responses << "\n";
812 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"403\"," << addrlabel << "} " << doh->d_http1Stats.d_nb403Responses << "\n";
813 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"500\"," << addrlabel << "} " << doh->d_http1Stats.d_nb500Responses << "\n";
814 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"502\"," << addrlabel << "} " << doh->d_http1Stats.d_nb502Responses << "\n";
815 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"other\"," << addrlabel << "} " << doh->d_http1Stats.d_nbOtherResponses << "\n";
816 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"200\"," << addrlabel << "} " << doh->d_http2Stats.d_nb200Responses << "\n";
817 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"400\"," << addrlabel << "} " << doh->d_http2Stats.d_nb400Responses << "\n";
818 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"403\"," << addrlabel << "} " << doh->d_http2Stats.d_nb403Responses << "\n";
819 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"500\"," << addrlabel << "} " << doh->d_http2Stats.d_nb500Responses << "\n";
820 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"502\"," << addrlabel << "} " << doh->d_http2Stats.d_nb502Responses << "\n";
821 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"other\"," << addrlabel << "} " << doh->d_http2Stats.d_nbOtherResponses << "\n";
822 }
823 #endif /* HAVE_DNS_OVER_HTTPS */
824
825 auto localPools = g_pools.getLocal();
826 const string cachebase = "dnsdist_pool_";
827 output << "# HELP dnsdist_pool_servers " << "Number of servers in that pool" << "\n";
828 output << "# TYPE dnsdist_pool_servers " << "gauge" << "\n";
829 output << "# HELP dnsdist_pool_active_servers " << "Number of available servers in that pool" << "\n";
830 output << "# TYPE dnsdist_pool_active_servers " << "gauge" << "\n";
831
832 output << "# HELP dnsdist_pool_cache_size " << "Maximum number of entries that this cache can hold" << "\n";
833 output << "# TYPE dnsdist_pool_cache_size " << "gauge" << "\n";
834 output << "# HELP dnsdist_pool_cache_entries " << "Number of entries currently present in that cache" << "\n";
835 output << "# TYPE dnsdist_pool_cache_entries " << "gauge" << "\n";
836 output << "# HELP dnsdist_pool_cache_hits " << "Number of hits from that cache" << "\n";
837 output << "# TYPE dnsdist_pool_cache_hits " << "counter" << "\n";
838 output << "# HELP dnsdist_pool_cache_misses " << "Number of misses from that cache" << "\n";
839 output << "# TYPE dnsdist_pool_cache_misses " << "counter" << "\n";
840 output << "# HELP dnsdist_pool_cache_deferred_inserts " << "Number of insertions into that cache skipped because it was already locked" << "\n";
841 output << "# TYPE dnsdist_pool_cache_deferred_inserts " << "counter" << "\n";
842 output << "# HELP dnsdist_pool_cache_deferred_lookups " << "Number of lookups into that cache skipped because it was already locked" << "\n";
843 output << "# TYPE dnsdist_pool_cache_deferred_lookups " << "counter" << "\n";
844 output << "# HELP dnsdist_pool_cache_lookup_collisions " << "Number of lookups into that cache that triggered a collision (same hash but different entry)" << "\n";
845 output << "# TYPE dnsdist_pool_cache_lookup_collisions " << "counter" << "\n";
846 output << "# HELP dnsdist_pool_cache_insert_collisions " << "Number of insertions into that cache that triggered a collision (same hash but different entry)" << "\n";
847 output << "# TYPE dnsdist_pool_cache_insert_collisions " << "counter" << "\n";
848 output << "# HELP dnsdist_pool_cache_ttl_too_shorts " << "Number of insertions into that cache skipped because the TTL of the answer was not long enough" << "\n";
849 output << "# TYPE dnsdist_pool_cache_ttl_too_shorts " << "counter" << "\n";
850 output << "# HELP dnsdist_pool_cache_cleanup_count_total " << "Number of times the cache has been scanned to remove expired entries, if any" << "\n";
851 output << "# TYPE dnsdist_pool_cache_cleanup_count_total " << "counter" << "\n";
852
853 for (const auto& entry : *localPools) {
854 string poolName = entry.first;
855
856 if (poolName.empty()) {
857 poolName = "_default_";
858 }
859 const string label = "{pool=\"" + poolName + "\"}";
860 const std::shared_ptr<ServerPool> pool = entry.second;
861 output << "dnsdist_pool_servers" << label << " " << pool->countServers(false) << "\n";
862 output << "dnsdist_pool_active_servers" << label << " " << pool->countServers(true) << "\n";
863
864 if (pool->packetCache != nullptr) {
865 const auto& cache = pool->packetCache;
866
867 output << cachebase << "cache_size" <<label << " " << cache->getMaxEntries() << "\n";
868 output << cachebase << "cache_entries" <<label << " " << cache->getEntriesCount() << "\n";
869 output << cachebase << "cache_hits" <<label << " " << cache->getHits() << "\n";
870 output << cachebase << "cache_misses" <<label << " " << cache->getMisses() << "\n";
871 output << cachebase << "cache_deferred_inserts" <<label << " " << cache->getDeferredInserts() << "\n";
872 output << cachebase << "cache_deferred_lookups" <<label << " " << cache->getDeferredLookups() << "\n";
873 output << cachebase << "cache_lookup_collisions" <<label << " " << cache->getLookupCollisions() << "\n";
874 output << cachebase << "cache_insert_collisions" <<label << " " << cache->getInsertCollisions() << "\n";
875 output << cachebase << "cache_ttl_too_shorts" <<label << " " << cache->getTTLTooShorts() << "\n";
876 output << cachebase << "cache_cleanup_count_total" <<label << " " << cache->getCleanupCount() << "\n";
877 }
878 }
879
880 output << "# HELP dnsdist_rule_hits " << "Number of hits of that rule" << "\n";
881 output << "# TYPE dnsdist_rule_hits " << "counter" << "\n";
882 addRulesToPrometheusOutput(output, g_ruleactions);
883 addRulesToPrometheusOutput(output, g_respruleactions);
884 addRulesToPrometheusOutput(output, g_cachehitrespruleactions);
885 addRulesToPrometheusOutput(output, g_cacheInsertedRespRuleActions);
886 addRulesToPrometheusOutput(output, g_selfansweredrespruleactions);
887
888 #ifndef DISABLE_DYNBLOCKS
889 output << "# HELP dnsdist_dynblocks_nmg_top_offenders_hits_per_second " << "Number of hits per second blocked by Dynamic Blocks (netmasks) for the top offenders, averaged over the last 60s" << "\n";
890 output << "# TYPE dnsdist_dynblocks_nmg_top_offenders_hits_per_second " << "gauge" << "\n";
891 auto topNetmasksByReason = DynBlockMaintenance::getHitsForTopNetmasks();
892 for (const auto& entry : topNetmasksByReason) {
893 for (const auto& netmask : entry.second) {
894 output << "dnsdist_dynblocks_nmg_top_offenders_hits_per_second{reason=\"" << entry.first << "\",netmask=\"" << netmask.first.toString() << "\"} " << netmask.second << "\n";
895 }
896 }
897
898 output << "# HELP dnsdist_dynblocks_smt_top_offenders_hits_per_second " << "Number of this per second blocked by Dynamic Blocks (suffixes) for the top offenders, averaged over the last 60s" << "\n";
899 output << "# TYPE dnsdist_dynblocks_smt_top_offenders_hits_per_second " << "gauge" << "\n";
900 auto topSuffixesByReason = DynBlockMaintenance::getHitsForTopSuffixes();
901 for (const auto& entry : topSuffixesByReason) {
902 for (const auto& suffix : entry.second) {
903 output << "dnsdist_dynblocks_smt_top_offenders_hits_per_second{reason=\"" << entry.first << "\",suffix=\"" << suffix.first.toString() << "\"} " << suffix.second << "\n";
904 }
905 }
906 #endif /* DISABLE_DYNBLOCKS */
907
908 output << "# HELP dnsdist_info " << "Info from dnsdist, value is always 1" << "\n";
909 output << "# TYPE dnsdist_info " << "gauge" << "\n";
910 output << "dnsdist_info{version=\"" << VERSION << "\"} " << "1" << "\n";
911
912 resp.body = output.str();
913 resp.headers["Content-Type"] = "text/plain";
914 }
915 #endif /* DISABLE_PROMETHEUS */
916
917 using namespace json11;
918
919 static void addStatsToJSONObject(Json::object& obj)
920 {
921 auto entries = dnsdist::metrics::g_stats.entries.read_lock();
922 for (const auto& entry : *entries) {
923 if (entry.d_name == "special-memory-usage") {
924 continue; // Too expensive for get-all
925 }
926 if (const auto& val = std::get_if<pdns::stat_t*>(&entry.d_value)) {
927 obj.emplace(entry.d_name, (double)(*val)->load());
928 } else if (const auto& adval = std::get_if<pdns::stat_t_trait<double>*>(&entry.d_value)) {
929 obj.emplace(entry.d_name, (*adval)->load());
930 } else if (const auto& dval = std::get_if<double*>(&entry.d_value)) {
931 obj.emplace(entry.d_name, (**dval));
932 } else if (const auto& func = std::get_if<dnsdist::metrics::Stats::statfunction_t>(&entry.d_value)) {
933 obj.emplace(entry.d_name, (double)(*func)(entry.d_name));
934 }
935 }
936 }
937
938 #ifndef DISABLE_BUILTIN_HTML
939 static void handleJSONStats(const YaHTTP::Request& req, YaHTTP::Response& resp)
940 {
941 handleCORS(req, resp);
942 resp.status = 200;
943
944 if (req.getvars.count("command") == 0) {
945 resp.status = 404;
946 return;
947 }
948
949 const string& command = req.getvars.at("command");
950
951 if (command == "stats") {
952 auto obj=Json::object {
953 { "packetcache-hits", 0},
954 { "packetcache-misses", 0},
955 { "over-capacity-drops", 0 },
956 { "too-old-drops", 0 },
957 { "server-policy", g_policy.getLocal()->getName()}
958 };
959
960 addStatsToJSONObject(obj);
961
962 Json my_json = obj;
963 resp.body = my_json.dump();
964 resp.headers["Content-Type"] = "application/json";
965 }
966 else if (command == "dynblocklist") {
967 Json::object obj;
968 #ifndef DISABLE_DYNBLOCKS
969 auto nmg = g_dynblockNMG.getLocal();
970 struct timespec now;
971 gettime(&now);
972 for (const auto& entry: *nmg) {
973 if (!(now < entry.second.until)) {
974 continue;
975 }
976 uint64_t counter = entry.second.blocks;
977 if (entry.second.bpf && g_defaultBPFFilter) {
978 counter += g_defaultBPFFilter->getHits(entry.first.getNetwork());
979 }
980 Json::object thing{
981 {"reason", entry.second.reason},
982 {"seconds", static_cast<double>(entry.second.until.tv_sec - now.tv_sec)},
983 {"blocks", static_cast<double>(counter)},
984 {"action", DNSAction::typeToString(entry.second.action != DNSAction::Action::None ? entry.second.action : g_dynBlockAction)},
985 {"warning", entry.second.warning},
986 {"ebpf", entry.second.bpf}
987 };
988 obj.emplace(entry.first.toString(), thing);
989 }
990
991 auto smt = g_dynblockSMT.getLocal();
992 smt->visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
993 if (!(now < node.d_value.until)) {
994 return;
995 }
996 string dom("empty");
997 if (!node.d_value.domain.empty()) {
998 dom = node.d_value.domain.toString();
999 }
1000 Json::object thing{
1001 {"reason", node.d_value.reason},
1002 {"seconds", static_cast<double>(node.d_value.until.tv_sec - now.tv_sec)},
1003 {"blocks", static_cast<double>(node.d_value.blocks)},
1004 {"action", DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction)},
1005 {"ebpf", node.d_value.bpf}
1006 };
1007 obj.emplace(dom, thing);
1008 });
1009 #endif /* DISABLE_DYNBLOCKS */
1010 Json my_json = obj;
1011 resp.body = my_json.dump();
1012 resp.headers["Content-Type"] = "application/json";
1013 }
1014 else if (command == "ebpfblocklist") {
1015 Json::object obj;
1016 #ifdef HAVE_EBPF
1017 struct timespec now;
1018 gettime(&now);
1019 for (const auto& dynbpf : g_dynBPFFilters) {
1020 std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > addrStats = dynbpf->getAddrStats();
1021 for (const auto& entry : addrStats) {
1022 Json::object thing
1023 {
1024 {"seconds", (double)(std::get<2>(entry).tv_sec - now.tv_sec)},
1025 {"blocks", (double)(std::get<1>(entry))}
1026 };
1027 obj.emplace(std::get<0>(entry).toString(), thing );
1028 }
1029 }
1030 if (g_defaultBPFFilter) {
1031 auto nmg = g_dynblockNMG.getLocal();
1032 for (const auto& entry: *nmg) {
1033 if (!(now < entry.second.until) || !entry.second.bpf) {
1034 continue;
1035 }
1036 uint64_t counter = entry.second.blocks + g_defaultBPFFilter->getHits(entry.first.getNetwork());
1037 Json::object thing{
1038 {"reason", entry.second.reason},
1039 {"seconds", static_cast<double>(entry.second.until.tv_sec - now.tv_sec)},
1040 {"blocks", static_cast<double>(counter)},
1041 {"action", DNSAction::typeToString(entry.second.action != DNSAction::Action::None ? entry.second.action : g_dynBlockAction)},
1042 {"warning", entry.second.warning},
1043 };
1044 obj.emplace(entry.first.toString(), thing);
1045 }
1046 }
1047 #endif /* HAVE_EBPF */
1048 Json my_json = obj;
1049 resp.body = my_json.dump();
1050 resp.headers["Content-Type"] = "application/json";
1051 }
1052 else {
1053 resp.status = 404;
1054 }
1055 }
1056 #endif /* DISABLE_BUILTIN_HTML */
1057
1058 static void addServerToJSON(Json::array& servers, int id, const std::shared_ptr<DownstreamState>& a)
1059 {
1060 string status;
1061 if (a->d_config.availability == DownstreamState::Availability::Up) {
1062 status = "UP";
1063 }
1064 else if (a->d_config.availability == DownstreamState::Availability::Down) {
1065 status = "DOWN";
1066 }
1067 else {
1068 status = (a->upStatus ? "up" : "down");
1069 }
1070
1071 Json::array pools;
1072 pools.reserve(a->d_config.pools.size());
1073 for (const auto& p: a->d_config.pools) {
1074 pools.push_back(p);
1075 }
1076
1077 Json::object server {
1078 {"id", id},
1079 {"name", a->getName()},
1080 {"address", a->d_config.remote.toStringWithPort()},
1081 {"state", status},
1082 {"protocol", a->getProtocol().toPrettyString()},
1083 {"qps", (double)a->queryLoad},
1084 {"qpsLimit", (double)a->qps.getRate()},
1085 {"outstanding", (double)a->outstanding},
1086 {"reuseds", (double)a->reuseds},
1087 {"weight", (double)a->d_config.d_weight},
1088 {"order", (double)a->d_config.order},
1089 {"pools", std::move(pools)},
1090 {"latency", (double)(a->latencyUsec/1000.0)},
1091 {"queries", (double)a->queries},
1092 {"responses", (double)a->responses},
1093 {"nonCompliantResponses", (double)a->nonCompliantResponses},
1094 {"sendErrors", (double)a->sendErrors},
1095 {"tcpDiedSendingQuery", (double)a->tcpDiedSendingQuery},
1096 {"tcpDiedReadingResponse", (double)a->tcpDiedReadingResponse},
1097 {"tcpGaveUp", (double)a->tcpGaveUp},
1098 {"tcpConnectTimeouts", (double)a->tcpConnectTimeouts},
1099 {"tcpReadTimeouts", (double)a->tcpReadTimeouts},
1100 {"tcpWriteTimeouts", (double)a->tcpWriteTimeouts},
1101 {"tcpCurrentConnections", (double)a->tcpCurrentConnections},
1102 {"tcpMaxConcurrentConnections", (double)a->tcpMaxConcurrentConnections},
1103 {"tcpTooManyConcurrentConnections", (double)a->tcpTooManyConcurrentConnections},
1104 {"tcpNewConnections", (double)a->tcpNewConnections},
1105 {"tcpReusedConnections", (double)a->tcpReusedConnections},
1106 {"tcpAvgQueriesPerConnection", (double)a->tcpAvgQueriesPerConnection},
1107 {"tcpAvgConnectionDuration", (double)a->tcpAvgConnectionDuration},
1108 {"tlsResumptions", (double)a->tlsResumptions},
1109 {"tcpLatency", (double)(a->latencyUsecTCP/1000.0)},
1110 {"healthCheckFailures", (double)(a->d_healthCheckMetrics.d_failures)},
1111 {"healthCheckFailuresParsing", (double)(a->d_healthCheckMetrics.d_parseErrors)},
1112 {"healthCheckFailuresTimeout", (double)(a->d_healthCheckMetrics.d_timeOuts)},
1113 {"healthCheckFailuresNetwork", (double)(a->d_healthCheckMetrics.d_networkErrors)},
1114 {"healthCheckFailuresMismatch", (double)(a->d_healthCheckMetrics.d_mismatchErrors)},
1115 {"healthCheckFailuresInvalid", (double)(a->d_healthCheckMetrics.d_invalidResponseErrors)},
1116 {"dropRate", (double)a->dropRate}
1117 };
1118
1119 /* sending a latency for a DOWN server doesn't make sense */
1120 if (a->d_config.availability == DownstreamState::Availability::Down) {
1121 server["latency"] = nullptr;
1122 server["tcpLatency"] = nullptr;
1123 }
1124
1125 servers.push_back(std::move(server));
1126 }
1127
1128 static void handleStats(const YaHTTP::Request& req, YaHTTP::Response& resp)
1129 {
1130 handleCORS(req, resp);
1131 resp.status = 200;
1132
1133 int num = 0;
1134
1135 Json::array servers;
1136 {
1137 auto localServers = g_dstates.getLocal();
1138 servers.reserve(localServers->size());
1139 for (const auto& a : *localServers) {
1140 addServerToJSON(servers, num++, a);
1141 }
1142 }
1143
1144 Json::array frontends;
1145 num = 0;
1146 frontends.reserve(g_frontends.size());
1147 for (const auto& front : g_frontends) {
1148 if (front->udpFD == -1 && front->tcpFD == -1)
1149 continue;
1150 Json::object frontend {
1151 { "id", num++ },
1152 { "address", front->local.toStringWithPort() },
1153 { "udp", front->udpFD >= 0 },
1154 { "tcp", front->tcpFD >= 0 },
1155 { "type", front->getType() },
1156 { "queries", (double) front->queries.load() },
1157 { "nonCompliantQueries", (double) front->nonCompliantQueries.load() },
1158 { "responses", (double) front->responses.load() },
1159 { "tcpDiedReadingQuery", (double) front->tcpDiedReadingQuery.load() },
1160 { "tcpDiedSendingResponse", (double) front->tcpDiedSendingResponse.load() },
1161 { "tcpGaveUp", (double) front->tcpGaveUp.load() },
1162 { "tcpClientTimeouts", (double) front->tcpClientTimeouts },
1163 { "tcpDownstreamTimeouts", (double) front->tcpDownstreamTimeouts },
1164 { "tcpCurrentConnections", (double) front->tcpCurrentConnections },
1165 { "tcpMaxConcurrentConnections", (double) front->tcpMaxConcurrentConnections },
1166 { "tcpAvgQueriesPerConnection", (double) front->tcpAvgQueriesPerConnection },
1167 { "tcpAvgConnectionDuration", (double) front->tcpAvgConnectionDuration },
1168 { "tlsNewSessions", (double) front->tlsNewSessions },
1169 { "tlsResumptions", (double) front->tlsResumptions },
1170 { "tlsUnknownTicketKey", (double) front->tlsUnknownTicketKey },
1171 { "tlsInactiveTicketKey", (double) front->tlsInactiveTicketKey },
1172 { "tls10Queries", (double) front->tls10queries },
1173 { "tls11Queries", (double) front->tls11queries },
1174 { "tls12Queries", (double) front->tls12queries },
1175 { "tls13Queries", (double) front->tls13queries },
1176 { "tlsUnknownQueries", (double) front->tlsUnknownqueries },
1177 };
1178 const TLSErrorCounters* errorCounters = nullptr;
1179 if (front->tlsFrontend != nullptr) {
1180 errorCounters = &front->tlsFrontend->d_tlsCounters;
1181 }
1182 else if (front->dohFrontend != nullptr) {
1183 errorCounters = &front->dohFrontend->d_tlsContext.d_tlsCounters;
1184 }
1185 if (errorCounters != nullptr) {
1186 frontend["tlsHandshakeFailuresDHKeyTooSmall"] = (double)errorCounters->d_dhKeyTooSmall;
1187 frontend["tlsHandshakeFailuresInappropriateFallBack"] = (double)errorCounters->d_inappropriateFallBack;
1188 frontend["tlsHandshakeFailuresNoSharedCipher"] = (double)errorCounters->d_noSharedCipher;
1189 frontend["tlsHandshakeFailuresUnknownCipher"] = (double)errorCounters->d_unknownCipherType;
1190 frontend["tlsHandshakeFailuresUnknownKeyExchangeType"] = (double)errorCounters->d_unknownKeyExchangeType;
1191 frontend["tlsHandshakeFailuresUnknownProtocol"] = (double)errorCounters->d_unknownProtocol;
1192 frontend["tlsHandshakeFailuresUnsupportedEC"] = (double)errorCounters->d_unsupportedEC;
1193 frontend["tlsHandshakeFailuresUnsupportedProtocol"] = (double)errorCounters->d_unsupportedProtocol;
1194 }
1195 frontends.push_back(std::move(frontend));
1196 }
1197
1198 Json::array dohs;
1199 #ifdef HAVE_DNS_OVER_HTTPS
1200 {
1201 dohs.reserve(g_dohlocals.size());
1202 num = 0;
1203 for (const auto& doh : g_dohlocals) {
1204 dohs.emplace_back(Json::object{
1205 { "id", num++ },
1206 { "address", doh->d_tlsContext.d_addr.toStringWithPort() },
1207 { "http-connects", (double) doh->d_httpconnects },
1208 { "http1-queries", (double) doh->d_http1Stats.d_nbQueries },
1209 { "http2-queries", (double) doh->d_http2Stats.d_nbQueries },
1210 { "http1-200-responses", (double) doh->d_http1Stats.d_nb200Responses },
1211 { "http2-200-responses", (double) doh->d_http2Stats.d_nb200Responses },
1212 { "http1-400-responses", (double) doh->d_http1Stats.d_nb400Responses },
1213 { "http2-400-responses", (double) doh->d_http2Stats.d_nb400Responses },
1214 { "http1-403-responses", (double) doh->d_http1Stats.d_nb403Responses },
1215 { "http2-403-responses", (double) doh->d_http2Stats.d_nb403Responses },
1216 { "http1-500-responses", (double) doh->d_http1Stats.d_nb500Responses },
1217 { "http2-500-responses", (double) doh->d_http2Stats.d_nb500Responses },
1218 { "http1-502-responses", (double) doh->d_http1Stats.d_nb502Responses },
1219 { "http2-502-responses", (double) doh->d_http2Stats.d_nb502Responses },
1220 { "http1-other-responses", (double) doh->d_http1Stats.d_nbOtherResponses },
1221 { "http2-other-responses", (double) doh->d_http2Stats.d_nbOtherResponses },
1222 { "get-queries", (double) doh->d_getqueries },
1223 { "post-queries", (double) doh->d_postqueries },
1224 { "bad-requests", (double) doh->d_badrequests },
1225 { "error-responses", (double) doh->d_errorresponses },
1226 { "redirect-responses", (double) doh->d_redirectresponses },
1227 { "valid-responses", (double) doh->d_validresponses }
1228 });
1229 }
1230 }
1231 #endif /* HAVE_DNS_OVER_HTTPS */
1232
1233 Json::array pools;
1234 {
1235 auto localPools = g_pools.getLocal();
1236 num = 0;
1237 pools.reserve(localPools->size());
1238 for (const auto& pool : *localPools) {
1239 const auto& cache = pool.second->packetCache;
1240 Json::object entry {
1241 { "id", num++ },
1242 { "name", pool.first },
1243 { "serversCount", (double) pool.second->countServers(false) },
1244 { "cacheSize", (double) (cache ? cache->getMaxEntries() : 0) },
1245 { "cacheEntries", (double) (cache ? cache->getEntriesCount() : 0) },
1246 { "cacheHits", (double) (cache ? cache->getHits() : 0) },
1247 { "cacheMisses", (double) (cache ? cache->getMisses() : 0) },
1248 { "cacheDeferredInserts", (double) (cache ? cache->getDeferredInserts() : 0) },
1249 { "cacheDeferredLookups", (double) (cache ? cache->getDeferredLookups() : 0) },
1250 { "cacheLookupCollisions", (double) (cache ? cache->getLookupCollisions() : 0) },
1251 { "cacheInsertCollisions", (double) (cache ? cache->getInsertCollisions() : 0) },
1252 { "cacheTTLTooShorts", (double) (cache ? cache->getTTLTooShorts() : 0) },
1253 { "cacheCleanupCount", (double) (cache ? cache->getCleanupCount() : 0) }
1254 };
1255 pools.push_back(std::move(entry));
1256 }
1257 }
1258
1259 Json::array rules;
1260 /* unfortunately DNSActions have getStats(),
1261 and DNSResponseActions do not. */
1262 {
1263 auto localRules = g_ruleactions.getLocal();
1264 num = 0;
1265 rules.reserve(localRules->size());
1266 for (const auto& a : *localRules) {
1267 Json::object rule{
1268 {"id", num++},
1269 {"creationOrder", (double)a.d_creationOrder},
1270 {"uuid", boost::uuids::to_string(a.d_id)},
1271 {"name", a.d_name},
1272 {"matches", (double)a.d_rule->d_matches},
1273 {"rule", a.d_rule->toString()},
1274 {"action", a.d_action->toString()},
1275 {"action-stats", a.d_action->getStats()}
1276 };
1277 rules.push_back(std::move(rule));
1278 }
1279 }
1280 auto responseRules = someResponseRulesToJson(&g_respruleactions);
1281 auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitrespruleactions);
1282 auto cacheInsertedResponseRules = someResponseRulesToJson(&g_cacheInsertedRespRuleActions);
1283 auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredrespruleactions);
1284
1285 string acl;
1286 {
1287 vector<string> vec;
1288 g_ACL.getLocal()->toStringVector(&vec);
1289
1290 for (const auto& s : vec) {
1291 if (!acl.empty()) {
1292 acl += ", ";
1293 }
1294 acl += s;
1295 }
1296 }
1297
1298 string localaddressesStr;
1299 {
1300 std::set<std::string> localaddresses;
1301 for (const auto& front : g_frontends) {
1302 localaddresses.insert(front->local.toStringWithPort());
1303 }
1304 for (const auto& addr : localaddresses) {
1305 if (!localaddressesStr.empty()) {
1306 localaddressesStr += ", ";
1307 }
1308 localaddressesStr += addr;
1309 }
1310 }
1311
1312 Json::object stats;
1313 addStatsToJSONObject(stats);
1314
1315 Json responseObject(Json::object({
1316 { "daemon_type", "dnsdist" },
1317 { "version", VERSION },
1318 { "servers", std::move(servers) },
1319 { "frontends", std::move(frontends) },
1320 { "pools", std::move(pools) },
1321 { "rules", std::move(rules) },
1322 { "response-rules", std::move(responseRules) },
1323 { "cache-hit-response-rules", std::move(cacheHitResponseRules) },
1324 { "cache-inserted-response-rules", std::move(cacheInsertedResponseRules) },
1325 { "self-answered-response-rules", std::move(selfAnsweredResponseRules) },
1326 { "acl", std::move(acl) },
1327 { "local", std::move(localaddressesStr) },
1328 { "dohFrontends", std::move(dohs) },
1329 { "statistics", std::move(stats) }
1330 }));
1331
1332 resp.headers["Content-Type"] = "application/json";
1333 resp.body = responseObject.dump();
1334 }
1335
1336 static void handlePoolStats(const YaHTTP::Request& req, YaHTTP::Response& resp)
1337 {
1338 handleCORS(req, resp);
1339 const auto poolName = req.getvars.find("name");
1340 if (poolName == req.getvars.end()) {
1341 resp.status = 400;
1342 return;
1343 }
1344
1345 resp.status = 200;
1346 Json::array doc;
1347
1348 auto localPools = g_pools.getLocal();
1349 const auto poolIt = localPools->find(poolName->second);
1350 if (poolIt == localPools->end()) {
1351 resp.status = 404;
1352 return;
1353 }
1354
1355 const auto& pool = poolIt->second;
1356 const auto& cache = pool->packetCache;
1357 Json::object entry {
1358 { "name", poolName->second },
1359 { "serversCount", (double) pool->countServers(false) },
1360 { "cacheSize", (double) (cache ? cache->getMaxEntries() : 0) },
1361 { "cacheEntries", (double) (cache ? cache->getEntriesCount() : 0) },
1362 { "cacheHits", (double) (cache ? cache->getHits() : 0) },
1363 { "cacheMisses", (double) (cache ? cache->getMisses() : 0) },
1364 { "cacheDeferredInserts", (double) (cache ? cache->getDeferredInserts() : 0) },
1365 { "cacheDeferredLookups", (double) (cache ? cache->getDeferredLookups() : 0) },
1366 { "cacheLookupCollisions", (double) (cache ? cache->getLookupCollisions() : 0) },
1367 { "cacheInsertCollisions", (double) (cache ? cache->getInsertCollisions() : 0) },
1368 { "cacheTTLTooShorts", (double) (cache ? cache->getTTLTooShorts() : 0) },
1369 { "cacheCleanupCount", (double) (cache ? cache->getCleanupCount() : 0) }
1370 };
1371
1372 Json::array servers;
1373 int num = 0;
1374 for (const auto& a : *pool->getServers()) {
1375 addServerToJSON(servers, num, a.second);
1376 num++;
1377 }
1378
1379 resp.headers["Content-Type"] = "application/json";
1380 Json my_json = Json::object {
1381 { "stats", entry },
1382 { "servers", servers }
1383 };
1384
1385 resp.body = my_json.dump();
1386 }
1387
1388 static void handleStatsOnly(const YaHTTP::Request& req, YaHTTP::Response& resp)
1389 {
1390 handleCORS(req, resp);
1391 resp.status = 200;
1392
1393 Json::array doc;
1394 {
1395 auto entries = dnsdist::metrics::g_stats.entries.read_lock();
1396 for (const auto& item : *entries) {
1397 if (item.d_name == "special-memory-usage") {
1398 continue; // Too expensive for get-all
1399 }
1400
1401 if (const auto& val = std::get_if<pdns::stat_t*>(&item.d_value)) {
1402 doc.push_back(Json::object {
1403 { "type", "StatisticItem" },
1404 { "name", item.d_name },
1405 { "value", (double)(*val)->load() }
1406 });
1407 }
1408 else if (const auto& adval = std::get_if<pdns::stat_t_trait<double>*>(&item.d_value)) {
1409 doc.push_back(Json::object {
1410 { "type", "StatisticItem" },
1411 { "name", item.d_name },
1412 { "value", (*adval)->load() }
1413 });
1414 }
1415 else if (const auto& dval = std::get_if<double*>(&item.d_value)) {
1416 doc.push_back(Json::object {
1417 { "type", "StatisticItem" },
1418 { "name", item.d_name },
1419 { "value", (**dval) }
1420 });
1421 }
1422 else if (const auto& func = std::get_if<dnsdist::metrics::Stats::statfunction_t>(&item.d_value)) {
1423 doc.push_back(Json::object {
1424 { "type", "StatisticItem" },
1425 { "name", item.d_name },
1426 { "value", (double)(*func)(item.d_name) }
1427 });
1428 }
1429 }
1430 }
1431 Json my_json = doc;
1432 resp.body = my_json.dump();
1433 resp.headers["Content-Type"] = "application/json";
1434 }
1435
1436 #ifndef DISABLE_WEB_CONFIG
1437 static void handleConfigDump(const YaHTTP::Request& req, YaHTTP::Response& resp)
1438 {
1439 handleCORS(req, resp);
1440 resp.status = 200;
1441
1442 Json::array doc;
1443 typedef boost::variant<bool, double, std::string> configentry_t;
1444 std::vector<std::pair<std::string, configentry_t> > configEntries {
1445 { "acl", g_ACL.getLocal()->toString() },
1446 { "allow-empty-response", g_allowEmptyResponse },
1447 { "control-socket", g_serverControl.toStringWithPort() },
1448 { "ecs-override", g_ECSOverride },
1449 { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
1450 { "ecs-source-prefix-v6", (double) g_ECSSourcePrefixV6 },
1451 { "fixup-case", g_fixupCase },
1452 { "max-outstanding", (double) g_maxOutstanding },
1453 { "server-policy", g_policy.getLocal()->getName() },
1454 { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
1455 { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
1456 { "tcp-send-timeout", (double) g_tcpSendTimeout },
1457 { "truncate-tc", g_truncateTC },
1458 { "verbose", g_verbose },
1459 { "verbose-health-checks", g_verboseHealthChecks }
1460 };
1461 for(const auto& item : configEntries) {
1462 if (const auto& bval = boost::get<bool>(&item.second)) {
1463 doc.push_back(Json::object {
1464 { "type", "ConfigSetting" },
1465 { "name", item.first },
1466 { "value", *bval }
1467 });
1468 }
1469 else if (const auto& sval = boost::get<string>(&item.second)) {
1470 doc.push_back(Json::object {
1471 { "type", "ConfigSetting" },
1472 { "name", item.first },
1473 { "value", *sval }
1474 });
1475 }
1476 else if (const auto& dval = boost::get<double>(&item.second)) {
1477 doc.push_back(Json::object {
1478 { "type", "ConfigSetting" },
1479 { "name", item.first },
1480 { "value", *dval }
1481 });
1482 }
1483 }
1484 Json my_json = doc;
1485 resp.body = my_json.dump();
1486 resp.headers["Content-Type"] = "application/json";
1487 }
1488
1489 static void handleAllowFrom(const YaHTTP::Request& req, YaHTTP::Response& resp)
1490 {
1491 handleCORS(req, resp);
1492
1493 resp.headers["Content-Type"] = "application/json";
1494 resp.status = 200;
1495
1496 if (req.method == "PUT") {
1497 std::string err;
1498 Json doc = Json::parse(req.body, err);
1499
1500 if (!doc.is_null()) {
1501 NetmaskGroup nmg;
1502 auto aclList = doc["value"];
1503 if (aclList.is_array()) {
1504
1505 for (const auto& value : aclList.array_items()) {
1506 try {
1507 nmg.addMask(value.string_value());
1508 } catch (NetmaskException &e) {
1509 resp.status = 400;
1510 break;
1511 }
1512 }
1513
1514 if (resp.status == 200) {
1515 infolog("Updating the ACL via the API to %s", nmg.toString());
1516 g_ACL.setState(nmg);
1517 apiSaveACL(nmg);
1518 }
1519 }
1520 else {
1521 resp.status = 400;
1522 }
1523 }
1524 else {
1525 resp.status = 400;
1526 }
1527 }
1528 if (resp.status == 200) {
1529 Json::array acl;
1530 vector<string> vec;
1531 g_ACL.getLocal()->toStringVector(&vec);
1532
1533 for(const auto& s : vec) {
1534 acl.push_back(s);
1535 }
1536
1537 Json::object obj{
1538 { "type", "ConfigSetting" },
1539 { "name", "allow-from" },
1540 { "value", acl }
1541 };
1542 Json my_json = obj;
1543 resp.body = my_json.dump();
1544 }
1545 }
1546 #endif /* DISABLE_WEB_CONFIG */
1547
1548 #ifndef DISABLE_WEB_CACHE_MANAGEMENT
1549 static void handleCacheManagement(const YaHTTP::Request& req, YaHTTP::Response& resp)
1550 {
1551 handleCORS(req, resp);
1552
1553 resp.headers["Content-Type"] = "application/json";
1554 resp.status = 200;
1555
1556 if (req.method != "DELETE") {
1557 resp.status = 400;
1558 Json::object obj{
1559 { "status", "denied" },
1560 { "error", "invalid method" }
1561 };
1562 resp.body = Json(obj).dump();
1563 return;
1564 }
1565
1566 const auto poolName = req.getvars.find("pool");
1567 const auto expungeName = req.getvars.find("name");
1568 const auto expungeType = req.getvars.find("type");
1569 const auto suffix = req.getvars.find("suffix");
1570 if (poolName == req.getvars.end() || expungeName == req.getvars.end()) {
1571 resp.status = 400;
1572 Json::object obj{
1573 { "status", "denied" },
1574 { "error", "missing 'pool' or 'name' parameter" },
1575 };
1576 resp.body = Json(obj).dump();
1577 return;
1578 }
1579
1580 DNSName name;
1581 QType type(QType::ANY);
1582 try {
1583 name = DNSName(expungeName->second);
1584 }
1585 catch (const std::exception& e) {
1586 resp.status = 400;
1587 Json::object obj{
1588 { "status", "error" },
1589 { "error", "unable to parse the requested name" },
1590 };
1591 resp.body = Json(obj).dump();
1592 return;
1593 }
1594 if (expungeType != req.getvars.end()) {
1595 type = QType::chartocode(expungeType->second.c_str());
1596 }
1597
1598 std::shared_ptr<ServerPool> pool;
1599 try {
1600 pool = getPool(g_pools.getCopy(), poolName->second);
1601 }
1602 catch (const std::exception& e) {
1603 resp.status = 404;
1604 Json::object obj{
1605 { "status", "not found" },
1606 { "error", "the requested pool does not exist" },
1607 };
1608 resp.body = Json(obj).dump();
1609 return;
1610 }
1611
1612 auto cache = pool->getCache();
1613 if (cache == nullptr) {
1614 resp.status = 404;
1615 Json::object obj{
1616 { "status", "not found" },
1617 { "error", "there is no cache associated with the requested pool" },
1618 };
1619 resp.body = Json(obj).dump();
1620 return;
1621 }
1622
1623 auto removed = cache->expungeByName(name, type.getCode(), suffix != req.getvars.end());
1624
1625 Json::object obj{
1626 { "status", "purged" },
1627 { "count", std::to_string(removed) }
1628 };
1629 resp.body = Json(obj).dump();
1630 }
1631 #endif /* DISABLE_WEB_CACHE_MANAGEMENT */
1632
1633 template<typename T> static void addRingEntryToList(const struct timespec& now, Json::array& list, const T& entry)
1634 {
1635 constexpr bool response = std::is_same_v<T, Rings::Response>;
1636 Json::object tmp{
1637 { "age", static_cast<double>(DiffTime(entry.when, now)) },
1638 { "id", ntohs(entry.dh.id) },
1639 { "name", entry.name.toString() },
1640 { "requestor", entry.requestor.toStringWithPort() },
1641 { "size", static_cast<int>(entry.size) },
1642 { "qtype", entry.qtype },
1643 { "protocol", entry.protocol.toString() },
1644 { "rd", static_cast<bool>(entry.dh.rd) },
1645 };
1646 if constexpr (!response) {
1647 #if defined(DNSDIST_RINGS_WITH_MACADDRESS)
1648 tmp.emplace("mac", entry.hasmac ? std::string(reinterpret_cast<const char*>(entry.macaddress.data()), entry.macaddress.size()) : std::string());
1649 #endif
1650 }
1651 else {
1652 tmp.emplace("latency", static_cast<double>(entry.usec));
1653 tmp.emplace("rcode", static_cast<uint8_t>(entry.dh.rcode));
1654 tmp.emplace("tc", static_cast<bool>(entry.dh.tc));
1655 tmp.emplace("aa", static_cast<bool>(entry.dh.aa));
1656 tmp.emplace("answers", ntohs(entry.dh.ancount));
1657 auto server = entry.ds.toStringWithPort();
1658 tmp.emplace("backend", server != "0.0.0.0:0" ? std::move(server) : "Cache");
1659 }
1660 list.push_back(std::move(tmp));
1661 }
1662
1663 static void handleRings(const YaHTTP::Request& req, YaHTTP::Response& resp)
1664 {
1665 handleCORS(req, resp);
1666
1667 std::optional<size_t> maxNumberOfQueries{std::nullopt};
1668 std::optional<size_t> maxNumberOfResponses{std::nullopt};
1669
1670 const auto maxQueries = req.getvars.find("maxQueries");
1671 if (maxQueries != req.getvars.end()) {
1672 try {
1673 maxNumberOfQueries = pdns::checked_stoi<size_t>(maxQueries->second);
1674 }
1675 catch (const std::exception& exp) {
1676 vinfolog("Error parsing the 'maxQueries' value from rings HTTP GET query: %s", exp.what());
1677 }
1678 }
1679
1680 const auto maxResponses = req.getvars.find("maxResponses");
1681 if (maxResponses != req.getvars.end()) {
1682 try {
1683 maxNumberOfResponses = pdns::checked_stoi<size_t>(maxResponses->second);
1684 }
1685 catch (const std::exception& exp) {
1686 vinfolog("Error parsing the 'maxResponses' value from rings HTTP GET query: %s", exp.what());
1687 }
1688 }
1689
1690 resp.status = 200;
1691
1692 Json::object doc;
1693 size_t numberOfQueries = 0;
1694 size_t numberOfResponses = 0;
1695 Json::array queries;
1696 Json::array responses;
1697 struct timespec now
1698 {
1699 };
1700 gettime(&now);
1701
1702 for (const auto& shard : g_rings.d_shards) {
1703 if (!maxNumberOfQueries || numberOfQueries < *maxNumberOfQueries) {
1704 auto queryRing = shard->queryRing.lock();
1705 for (const auto& entry : *queryRing) {
1706 addRingEntryToList(now, queries, entry);
1707 numberOfQueries++;
1708 if (maxNumberOfQueries && numberOfQueries >= *maxNumberOfQueries) {
1709 break;
1710 }
1711 }
1712 }
1713 if (!maxNumberOfResponses || numberOfResponses < *maxNumberOfResponses) {
1714 auto responseRing = shard->respRing.lock();
1715 for (const auto& entry : *responseRing) {
1716 addRingEntryToList(now, responses, entry);
1717 numberOfResponses++;
1718 if (maxNumberOfResponses && numberOfResponses >= *maxNumberOfResponses) {
1719 break;
1720 }
1721 }
1722 }
1723 }
1724 doc.emplace("queries", std::move(queries));
1725 doc.emplace("responses", std::move(responses));
1726 Json my_json = doc;
1727 resp.body = my_json.dump();
1728 resp.headers["Content-Type"] = "application/json";
1729 }
1730
1731 static std::unordered_map<std::string, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)>> s_webHandlers;
1732
1733 void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> handler);
1734
1735 void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> handler)
1736 {
1737 s_webHandlers[endpoint] = std::move(handler);
1738 }
1739
1740 void clearWebHandlers()
1741 {
1742 s_webHandlers.clear();
1743 }
1744
1745 #ifndef DISABLE_BUILTIN_HTML
1746 #include "htmlfiles.h"
1747
1748 static void redirectToIndex(const YaHTTP::Request& req, YaHTTP::Response& resp)
1749 {
1750 const string charset = "; charset=utf-8";
1751 resp.body.assign(s_urlmap.at("index.html"));
1752 resp.headers["Content-Type"] = "text/html" + charset;
1753 resp.status = 200;
1754 }
1755
1756 static void handleBuiltInFiles(const YaHTTP::Request& req, YaHTTP::Response& resp)
1757 {
1758 if (req.url.path.empty() || !s_urlmap.count(req.url.path.c_str()+1)) {
1759 resp.status = 404;
1760 return;
1761 }
1762
1763 resp.body.assign(s_urlmap.at(req.url.path.c_str()+1));
1764
1765 vector<string> parts;
1766 stringtok(parts, req.url.path, ".");
1767 static const std::unordered_map<std::string, std::string> contentTypeMap = {
1768 { "html", "text/html" },
1769 { "css", "text/css" },
1770 { "js", "application/javascript" },
1771 { "png", "image/png" },
1772 };
1773
1774 const auto& it = contentTypeMap.find(parts.back());
1775 if (it != contentTypeMap.end()) {
1776 const string charset = "; charset=utf-8";
1777 resp.headers["Content-Type"] = it->second + charset;
1778 }
1779
1780 resp.status = 200;
1781 }
1782 #endif /* DISABLE_BUILTIN_HTML */
1783
1784 void registerBuiltInWebHandlers()
1785 {
1786 #ifndef DISABLE_BUILTIN_HTML
1787 registerWebHandler("/jsonstat", handleJSONStats);
1788 #endif /* DISABLE_BUILTIN_HTML */
1789 #ifndef DISABLE_PROMETHEUS
1790 registerWebHandler("/metrics", handlePrometheus);
1791 #endif /* DISABLE_PROMETHEUS */
1792 registerWebHandler("/api/v1/servers/localhost", handleStats);
1793 registerWebHandler("/api/v1/servers/localhost/pool", handlePoolStats);
1794 registerWebHandler("/api/v1/servers/localhost/statistics", handleStatsOnly);
1795 registerWebHandler("/api/v1/servers/localhost/rings", handleRings);
1796 #ifndef DISABLE_WEB_CONFIG
1797 registerWebHandler("/api/v1/servers/localhost/config", handleConfigDump);
1798 registerWebHandler("/api/v1/servers/localhost/config/allow-from", handleAllowFrom);
1799 #endif /* DISABLE_WEB_CONFIG */
1800 #ifndef DISABLE_WEB_CACHE_MANAGEMENT
1801 registerWebHandler("/api/v1/cache", handleCacheManagement);
1802 #endif /* DISABLE_WEB_CACHE_MANAGEMENT */
1803 #ifndef DISABLE_BUILTIN_HTML
1804 registerWebHandler("/", redirectToIndex);
1805
1806 for (const auto& path : s_urlmap) {
1807 registerWebHandler("/" + path.first, handleBuiltInFiles);
1808 }
1809 #endif /* DISABLE_BUILTIN_HTML */
1810 }
1811
1812 static void connectionThread(WebClientConnection&& conn)
1813 {
1814 setThreadName("dnsdist/webConn");
1815
1816 vinfolog("Webserver handling connection from %s", conn.getClient().toStringWithPort());
1817
1818 try {
1819 YaHTTP::AsyncRequestLoader yarl;
1820 YaHTTP::Request req;
1821 bool finished = false;
1822
1823 yarl.initialize(&req);
1824 while (!finished) {
1825 int bytes;
1826 char buf[1024];
1827 bytes = read(conn.getSocket().getHandle(), buf, sizeof(buf));
1828 if (bytes > 0) {
1829 string data = string(buf, bytes);
1830 finished = yarl.feed(data);
1831 } else {
1832 // read error OR EOF
1833 break;
1834 }
1835 }
1836 yarl.finalize();
1837
1838 req.getvars.erase("_"); // jQuery cache buster
1839
1840 YaHTTP::Response resp;
1841 resp.version = req.version;
1842
1843 {
1844 auto config = g_webserverConfig.lock();
1845
1846 addCustomHeaders(resp, config->customHeaders);
1847 addSecurityHeaders(resp, config->customHeaders);
1848 }
1849 /* indicate that the connection will be closed after completion of the response */
1850 resp.headers["Connection"] = "close";
1851
1852 /* no need to send back the API key if any */
1853 resp.headers.erase("X-API-Key");
1854
1855 if (req.method == "OPTIONS") {
1856 /* the OPTIONS method should not require auth, otherwise it breaks CORS */
1857 handleCORS(req, resp);
1858 resp.status = 200;
1859 }
1860 else if (!handleAuthorization(req)) {
1861 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
1862 if (header != req.headers.end()) {
1863 vinfolog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, conn.getClient().toStringWithPort());
1864 }
1865 resp.status = 401;
1866 resp.body = "<h1>Unauthorized</h1>";
1867 resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
1868 }
1869 else if (!isMethodAllowed(req)) {
1870 resp.status = 405;
1871 }
1872 else {
1873 const auto it = s_webHandlers.find(req.url.path);
1874 if (it != s_webHandlers.end()) {
1875 it->second(req, resp);
1876 }
1877 else {
1878 resp.status = 404;
1879 }
1880 }
1881
1882 std::ostringstream ofs;
1883 ofs << resp;
1884 string done = ofs.str();
1885 writen2(conn.getSocket().getHandle(), done.c_str(), done.size());
1886 }
1887 catch (const YaHTTP::ParseError& e) {
1888 vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", conn.getClient().toStringWithPort(), e.what());
1889 }
1890 catch (const std::exception& e) {
1891 vinfolog("Webserver thread died with exception while processing a request from %s: %s", conn.getClient().toStringWithPort(), e.what());
1892 }
1893 catch (...) {
1894 vinfolog("Webserver thread died with exception while processing a request from %s", conn.getClient().toStringWithPort());
1895 }
1896 }
1897
1898 void setWebserverAPIKey(std::unique_ptr<CredentialsHolder>&& apiKey)
1899 {
1900 auto config = g_webserverConfig.lock();
1901
1902 if (apiKey) {
1903 config->apiKey = std::move(apiKey);
1904 } else {
1905 config->apiKey.reset();
1906 }
1907 }
1908
1909 void setWebserverPassword(std::unique_ptr<CredentialsHolder>&& password)
1910 {
1911 g_webserverConfig.lock()->password = std::move(password);
1912 }
1913
1914 void setWebserverACL(const std::string& acl)
1915 {
1916 NetmaskGroup newACL;
1917 newACL.toMasks(acl);
1918
1919 g_webserverConfig.lock()->acl = std::move(newACL);
1920 }
1921
1922 void setWebserverCustomHeaders(const boost::optional<std::unordered_map<std::string, std::string> > customHeaders)
1923 {
1924 g_webserverConfig.lock()->customHeaders = customHeaders;
1925 }
1926
1927 void setWebserverStatsRequireAuthentication(bool require)
1928 {
1929 g_webserverConfig.lock()->statsRequireAuthentication = require;
1930 }
1931
1932 void setWebserverAPIRequiresAuthentication(bool require)
1933 {
1934 g_webserverConfig.lock()->apiRequiresAuthentication = require;
1935 }
1936
1937 void setWebserverDashboardRequiresAuthentication(bool require)
1938 {
1939 g_webserverConfig.lock()->dashboardRequiresAuthentication = require;
1940 }
1941
1942 void setWebserverMaxConcurrentConnections(size_t max)
1943 {
1944 s_connManager.setMaxConcurrentConnections(max);
1945 }
1946
1947 void dnsdistWebserverThread(int sock, const ComboAddress& local)
1948 {
1949 setThreadName("dnsdist/webserv");
1950 infolog("Webserver launched on %s", local.toStringWithPort());
1951
1952 {
1953 auto config = g_webserverConfig.lock();
1954 if (!config->password && config->dashboardRequiresAuthentication) {
1955 warnlog("Webserver launched on %s without a password set!", local.toStringWithPort());
1956 }
1957 }
1958
1959 for (;;) {
1960 try {
1961 ComboAddress remote(local);
1962 int fd = SAccept(sock, remote);
1963
1964 if (!isClientAllowedByACL(remote)) {
1965 vinfolog("Connection to webserver from client %s is not allowed, closing", remote.toStringWithPort());
1966 close(fd);
1967 continue;
1968 }
1969
1970 WebClientConnection conn(remote, fd);
1971 vinfolog("Got a connection to the webserver from %s", remote.toStringWithPort());
1972
1973 std::thread t(connectionThread, std::move(conn));
1974 t.detach();
1975 }
1976 catch (const std::exception& e) {
1977 vinfolog("Had an error accepting new webserver connection: %s", e.what());
1978 }
1979 }
1980 }