]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/dnsdist-web.cc
Merge pull request #9229 from rgacogne/dnsdist-webserver-allow-from
[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/incbin/incbin.h"
30 #include "ext/json11/json11.hpp"
31 #include <yahttp/yahttp.hpp>
32
33 #include "base64.hh"
34 #include "dnsdist.hh"
35 #include "dnsdist-healthchecks.hh"
36 #include "dnsdist-prometheus.hh"
37 #include "dnsdist-web.hh"
38 #include "dolog.hh"
39 #include "gettime.hh"
40 #include "htmlfiles.h"
41 #include "threadname.hh"
42 #include "sstuff.hh"
43
44 bool g_apiReadWrite{false};
45 WebserverConfig g_webserverConfig;
46 std::string g_apiConfigDirectory;
47 static const MetricDefinitionStorage s_metricDefinitions;
48
49 const std::map<std::string, MetricDefinition> MetricDefinitionStorage::metrics{
50 { "responses", MetricDefinition(PrometheusMetricType::counter, "Number of responses received from backends") },
51 { "servfail-responses", MetricDefinition(PrometheusMetricType::counter, "Number of SERVFAIL answers received from backends") },
52 { "queries", MetricDefinition(PrometheusMetricType::counter, "Number of received queries")},
53 { "frontend-nxdomain", MetricDefinition(PrometheusMetricType::counter, "Number of NXDomain answers sent to clients")},
54 { "frontend-servfail", MetricDefinition(PrometheusMetricType::counter, "Number of SERVFAIL answers sent to clients")},
55 { "frontend-noerror", MetricDefinition(PrometheusMetricType::counter, "Number of NoError answers sent to clients")},
56 { "acl-drops", MetricDefinition(PrometheusMetricType::counter, "Number of packets dropped because of the ACL")},
57 { "rule-drop", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because of a rule")},
58 { "rule-nxdomain", MetricDefinition(PrometheusMetricType::counter, "Number of NXDomain answers returned because of a rule")},
59 { "rule-refused", MetricDefinition(PrometheusMetricType::counter, "Number of Refused answers returned because of a rule")},
60 { "rule-servfail", MetricDefinition(PrometheusMetricType::counter, "Number of SERVFAIL answers received because of a rule")},
61 { "self-answered", MetricDefinition(PrometheusMetricType::counter, "Number of self-answered responses")},
62 { "downstream-timeouts", MetricDefinition(PrometheusMetricType::counter, "Number of queries not answered in time by a backend")},
63 { "downstream-send-errors", MetricDefinition(PrometheusMetricType::counter, "Number of errors when sending a query to a backend")},
64 { "trunc-failures", MetricDefinition(PrometheusMetricType::counter, "Number of errors encountered while truncating an answer")},
65 { "no-policy", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because no server was available")},
66 { "latency0-1", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in less than 1ms")},
67 { "latency1-10", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 1-10 ms")},
68 { "latency10-50", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 10-50 ms")},
69 { "latency50-100", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 50-100 ms")},
70 { "latency100-1000", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in 100-1000 ms")},
71 { "latency-slow", MetricDefinition(PrometheusMetricType::counter, "Number of queries answered in more than 1 second")},
72 { "latency-avg100", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 100 packets")},
73 { "latency-avg1000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 1000 packets")},
74 { "latency-avg10000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 10000 packets")},
75 { "latency-avg1000000", MetricDefinition(PrometheusMetricType::gauge, "Average response latency in microseconds of the last 1000000 packets")},
76 { "uptime", MetricDefinition(PrometheusMetricType::gauge, "Uptime of the dnsdist process in seconds")},
77 { "real-memory-usage", MetricDefinition(PrometheusMetricType::gauge, "Current memory usage in bytes")},
78 { "noncompliant-queries", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped as non-compliant")},
79 { "noncompliant-responses", MetricDefinition(PrometheusMetricType::counter, "Number of answers from a backend dropped as non-compliant")},
80 { "rdqueries", MetricDefinition(PrometheusMetricType::counter, "Number of received queries with the recursion desired bit set")},
81 { "empty-queries", MetricDefinition(PrometheusMetricType::counter, "Number of empty queries received from clients")},
82 { "cache-hits", MetricDefinition(PrometheusMetricType::counter, "Number of times an answer was retrieved from cache")},
83 { "cache-misses", MetricDefinition(PrometheusMetricType::counter, "Number of times an answer not found in the cache")},
84 { "cpu-iowait", MetricDefinition(PrometheusMetricType::counter, "Time waiting for I/O to complete by the whole system, in units of USER_HZ")},
85 { "cpu-user-msec", MetricDefinition(PrometheusMetricType::counter, "Milliseconds spent by dnsdist in the user state")},
86 { "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")},
87 { "cpu-sys-msec", MetricDefinition(PrometheusMetricType::counter, "Milliseconds spent by dnsdist in the system state")},
88 { "fd-usage", MetricDefinition(PrometheusMetricType::gauge, "Number of currently used file descriptors")},
89 { "dyn-blocked", MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because of a dynamic block")},
90 { "dyn-block-nmg-size", MetricDefinition(PrometheusMetricType::gauge, "Number of dynamic blocks entries") },
91 { "security-status", MetricDefinition(PrometheusMetricType::gauge, "Security status of this software. 0=unknown, 1=OK, 2=upgrade recommended, 3=upgrade mandatory") },
92 { "doh-query-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of DoH queries dropped because the internal pipe used to distribute queries was full") },
93 { "doh-response-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of DoH responses dropped because the internal pipe used to distribute responses was full") },
94 { "udp-in-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp InErrors") },
95 { "udp-noport-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp NoPorts") },
96 { "udp-recvbuf-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp RcvbufErrors") },
97 { "udp-sndbuf-errors", MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp SndbufErrors") },
98 };
99
100 static bool apiWriteConfigFile(const string& filebasename, const string& content)
101 {
102 if (!g_apiReadWrite) {
103 errlog("Not writing content to %s since the API is read-only", filebasename);
104 return false;
105 }
106
107 if (g_apiConfigDirectory.empty()) {
108 vinfolog("Not writing content to %s since the API configuration directory is not set", filebasename);
109 return false;
110 }
111
112 string filename = g_apiConfigDirectory + "/" + filebasename + ".conf";
113 ofstream ofconf(filename.c_str());
114 if (!ofconf) {
115 errlog("Could not open configuration fragment file '%s' for writing: %s", filename, stringerror());
116 return false;
117 }
118 ofconf << "-- Generated by the REST API, DO NOT EDIT" << endl;
119 ofconf << content << endl;
120 ofconf.close();
121 return true;
122 }
123
124 static void apiSaveACL(const NetmaskGroup& nmg)
125 {
126 vector<string> vec;
127 nmg.toStringVector(&vec);
128
129 string acl;
130 for(const auto& s : vec) {
131 if (!acl.empty()) {
132 acl += ", ";
133 }
134 acl += "\"" + s + "\"";
135 }
136
137 string content = "setACL({" + acl + "})";
138 apiWriteConfigFile("acl", content);
139 }
140
141 static bool checkAPIKey(const YaHTTP::Request& req, const string& expectedApiKey)
142 {
143 if (expectedApiKey.empty()) {
144 return false;
145 }
146
147 const auto header = req.headers.find("x-api-key");
148 if (header != req.headers.end()) {
149 return (header->second == expectedApiKey);
150 }
151
152 return false;
153 }
154
155 static bool checkWebPassword(const YaHTTP::Request& req, const string &expected_password)
156 {
157 static const char basicStr[] = "basic ";
158
159 const auto header = req.headers.find("authorization");
160
161 if (header != req.headers.end() && toLower(header->second).find(basicStr) == 0) {
162 string cookie = header->second.substr(sizeof(basicStr) - 1);
163
164 string plain;
165 B64Decode(cookie, plain);
166
167 vector<string> cparts;
168 stringtok(cparts, plain, ":");
169
170 if (cparts.size() == 2) {
171 return cparts[1] == expected_password;
172 }
173 }
174
175 return false;
176 }
177
178 static bool isAnAPIRequest(const YaHTTP::Request& req)
179 {
180 return req.url.path.find("/api/") == 0;
181 }
182
183 static bool isAnAPIRequestAllowedWithWebAuth(const YaHTTP::Request& req)
184 {
185 return req.url.path == "/api/v1/servers/localhost";
186 }
187
188 static bool isAStatsRequest(const YaHTTP::Request& req)
189 {
190 return req.url.path == "/jsonstat" || req.url.path == "/metrics";
191 }
192
193 static bool compareAuthorization(const YaHTTP::Request& req)
194 {
195 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
196
197 if (isAnAPIRequest(req)) {
198 /* Access to the API requires a valid API key */
199 if (checkAPIKey(req, g_webserverConfig.apiKey)) {
200 return true;
201 }
202
203 return isAnAPIRequestAllowedWithWebAuth(req) && checkWebPassword(req, g_webserverConfig.password);
204 }
205
206 if (isAStatsRequest(req)) {
207 /* Access to the stats is allowed for both API and Web users */
208 return checkAPIKey(req, g_webserverConfig.apiKey) || checkWebPassword(req, g_webserverConfig.password);
209 }
210
211 return checkWebPassword(req, g_webserverConfig.password);
212 }
213
214 static bool isMethodAllowed(const YaHTTP::Request& req)
215 {
216 if (req.method == "GET") {
217 return true;
218 }
219 if (req.method == "PUT" && g_apiReadWrite) {
220 if (req.url.path == "/api/v1/servers/localhost/config/allow-from") {
221 return true;
222 }
223 }
224 return false;
225 }
226
227 static bool isClientAllowedByACL(const ComboAddress& remote)
228 {
229 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
230 return g_webserverConfig.acl.match(remote);
231 }
232
233 static void handleCORS(const YaHTTP::Request& req, YaHTTP::Response& resp)
234 {
235 const auto origin = req.headers.find("Origin");
236 if (origin != req.headers.end()) {
237 if (req.method == "OPTIONS") {
238 /* Pre-flight request */
239 if (g_apiReadWrite) {
240 resp.headers["Access-Control-Allow-Methods"] = "GET, PUT";
241 }
242 else {
243 resp.headers["Access-Control-Allow-Methods"] = "GET";
244 }
245 resp.headers["Access-Control-Allow-Headers"] = "Authorization, X-API-Key";
246 }
247
248 resp.headers["Access-Control-Allow-Origin"] = origin->second;
249
250 if (isAStatsRequest(req) || isAnAPIRequestAllowedWithWebAuth(req)) {
251 resp.headers["Access-Control-Allow-Credentials"] = "true";
252 }
253 }
254 }
255
256 static void addSecurityHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
257 {
258 static const std::vector<std::pair<std::string, std::string> > headers = {
259 { "X-Content-Type-Options", "nosniff" },
260 { "X-Frame-Options", "deny" },
261 { "X-Permitted-Cross-Domain-Policies", "none" },
262 { "X-XSS-Protection", "1; mode=block" },
263 { "Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'" },
264 };
265
266 for (const auto& h : headers) {
267 if (customHeaders) {
268 const auto& custom = customHeaders->find(h.first);
269 if (custom != customHeaders->end()) {
270 continue;
271 }
272 }
273 resp.headers[h.first] = h.second;
274 }
275 }
276
277 static void addCustomHeaders(YaHTTP::Response& resp, const boost::optional<std::map<std::string, std::string> >& customHeaders)
278 {
279 if (!customHeaders)
280 return;
281
282 for (const auto& c : *customHeaders) {
283 if (!c.second.empty()) {
284 resp.headers[c.first] = c.second;
285 }
286 }
287 }
288
289 template<typename T>
290 static json11::Json::array someResponseRulesToJson(GlobalStateHolder<vector<T>>* someResponseRules)
291 {
292 using namespace json11;
293 Json::array responseRules;
294 int num=0;
295 auto localResponseRules = someResponseRules->getLocal();
296 for(const auto& a : *localResponseRules) {
297 Json::object rule{
298 {"id", num++},
299 {"creationOrder", (double)a.d_creationOrder},
300 {"uuid", boost::uuids::to_string(a.d_id)},
301 {"matches", (double)a.d_rule->d_matches},
302 {"rule", a.d_rule->toString()},
303 {"action", a.d_action->toString()},
304 };
305 responseRules.push_back(rule);
306 }
307 return responseRules;
308 }
309
310 static void connectionThread(int sock, ComboAddress remote)
311 {
312 setThreadName("dnsdist/webConn");
313
314 using namespace json11;
315 vinfolog("Webserver handling connection from %s", remote.toStringWithPort());
316
317 try {
318 YaHTTP::AsyncRequestLoader yarl;
319 YaHTTP::Request req;
320 bool finished = false;
321
322 yarl.initialize(&req);
323 while(!finished) {
324 int bytes;
325 char buf[1024];
326 bytes = read(sock, buf, sizeof(buf));
327 if (bytes > 0) {
328 string data = string(buf, bytes);
329 finished = yarl.feed(data);
330 } else {
331 // read error OR EOF
332 break;
333 }
334 }
335 yarl.finalize();
336
337 string command=req.getvars["command"];
338
339 req.getvars.erase("_"); // jQuery cache buster
340
341 YaHTTP::Response resp;
342 resp.version = req.version;
343 const string charset = "; charset=utf-8";
344
345 {
346 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
347
348 addCustomHeaders(resp, g_webserverConfig.customHeaders);
349 addSecurityHeaders(resp, g_webserverConfig.customHeaders);
350 }
351 /* indicate that the connection will be closed after completion of the response */
352 resp.headers["Connection"] = "close";
353
354 /* no need to send back the API key if any */
355 resp.headers.erase("X-API-Key");
356
357 if(req.method == "OPTIONS") {
358 /* the OPTIONS method should not require auth, otherwise it breaks CORS */
359 handleCORS(req, resp);
360 resp.status=200;
361 }
362 else if (!compareAuthorization(req)) {
363 YaHTTP::strstr_map_t::iterator header = req.headers.find("authorization");
364 if (header != req.headers.end())
365 errlog("HTTP Request \"%s\" from %s: Web Authentication failed", req.url.path, remote.toStringWithPort());
366 resp.status=401;
367 resp.body="<h1>Unauthorized</h1>";
368 resp.headers["WWW-Authenticate"] = "basic realm=\"PowerDNS\"";
369
370 }
371 else if(!isMethodAllowed(req)) {
372 resp.status=405;
373 }
374 else if(req.url.path=="/jsonstat") {
375 handleCORS(req, resp);
376 resp.status=200;
377
378 if(command=="stats") {
379 auto obj=Json::object {
380 { "packetcache-hits", 0},
381 { "packetcache-misses", 0},
382 { "over-capacity-drops", 0 },
383 { "too-old-drops", 0 },
384 { "server-policy", g_policy.getLocal()->name}
385 };
386
387 for(const auto& e : g_stats.entries) {
388 if (e.first == "special-memory-usage")
389 continue; // Too expensive for get-all
390 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&e.second))
391 obj.insert({e.first, (double)(*val)->load()});
392 else if (const auto& dval = boost::get<double*>(&e.second))
393 obj.insert({e.first, (**dval)});
394 else
395 obj.insert({e.first, (double)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
396 }
397 Json my_json = obj;
398 resp.body=my_json.dump();
399 resp.headers["Content-Type"] = "application/json";
400 }
401 else if(command=="dynblocklist") {
402 Json::object obj;
403 auto nmg = g_dynblockNMG.getLocal();
404 struct timespec now;
405 gettime(&now);
406 for(const auto& e: *nmg) {
407 if(now < e.second.until ) {
408 Json::object thing{
409 {"reason", e.second.reason},
410 {"seconds", (double)(e.second.until.tv_sec - now.tv_sec)},
411 {"blocks", (double)e.second.blocks},
412 {"action", DNSAction::typeToString(e.second.action != DNSAction::Action::None ? e.second.action : g_dynBlockAction) },
413 {"warning", e.second.warning }
414 };
415 obj.insert({e.first.toString(), thing});
416 }
417 }
418
419 auto smt = g_dynblockSMT.getLocal();
420 smt->visit([&now,&obj](const SuffixMatchTree<DynBlock>& node) {
421 if(now <node.d_value.until) {
422 string dom("empty");
423 if(!node.d_value.domain.empty())
424 dom = node.d_value.domain.toString();
425 Json::object thing{
426 {"reason", node.d_value.reason},
427 {"seconds", (double)(node.d_value.until.tv_sec - now.tv_sec)},
428 {"blocks", (double)node.d_value.blocks},
429 {"action", DNSAction::typeToString(node.d_value.action != DNSAction::Action::None ? node.d_value.action : g_dynBlockAction) }
430 };
431 obj.insert({dom, thing});
432 }
433 });
434
435
436
437 Json my_json = obj;
438 resp.body=my_json.dump();
439 resp.headers["Content-Type"] = "application/json";
440 }
441 else if(command=="ebpfblocklist") {
442 Json::object obj;
443 #ifdef HAVE_EBPF
444 struct timespec now;
445 gettime(&now);
446 for (const auto& dynbpf : g_dynBPFFilters) {
447 std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > addrStats = dynbpf->getAddrStats();
448 for (const auto& entry : addrStats) {
449 Json::object thing
450 {
451 {"seconds", (double)(std::get<2>(entry).tv_sec - now.tv_sec)},
452 {"blocks", (double)(std::get<1>(entry))}
453 };
454 obj.insert({std::get<0>(entry).toString(), thing });
455 }
456 }
457 #endif /* HAVE_EBPF */
458 Json my_json = obj;
459 resp.body=my_json.dump();
460 resp.headers["Content-Type"] = "application/json";
461 }
462 else {
463 resp.status=404;
464 }
465 }
466 else if (req.url.path == "/metrics") {
467 handleCORS(req, resp);
468 resp.status = 200;
469
470 std::ostringstream output;
471 static const std::set<std::string> metricBlacklist = { "latency-count", "latency-sum" };
472 for (const auto& e : g_stats.entries) {
473 if (e.first == "special-memory-usage")
474 continue; // Too expensive for get-all
475 std::string metricName = std::get<0>(e);
476
477 // Prometheus suggest using '_' instead of '-'
478 std::string prometheusMetricName = "dnsdist_" + boost::replace_all_copy(metricName, "-", "_");
479 if (metricBlacklist.count(metricName) != 0) {
480 continue;
481 }
482
483 MetricDefinition metricDetails;
484 if (!s_metricDefinitions.getMetricDetails(metricName, metricDetails)) {
485 vinfolog("Do not have metric details for %s", metricName);
486 continue;
487 }
488
489 std::string prometheusTypeName = s_metricDefinitions.getPrometheusStringMetricType(metricDetails.prometheusType);
490
491 if (prometheusTypeName == "") {
492 vinfolog("Unknown Prometheus type for %s", metricName);
493 continue;
494 }
495
496 // for these we have the help and types encoded in the sources:
497 output << "# HELP " << prometheusMetricName << " " << metricDetails.description << "\n";
498 output << "# TYPE " << prometheusMetricName << " " << prometheusTypeName << "\n";
499 output << prometheusMetricName << " ";
500
501 if (const auto& val = boost::get<DNSDistStats::stat_t*>(&std::get<1>(e)))
502 output << (*val)->load();
503 else if (const auto& dval = boost::get<double*>(&std::get<1>(e)))
504 output << **dval;
505 else
506 output << (*boost::get<DNSDistStats::statfunction_t>(&std::get<1>(e)))(std::get<0>(e));
507
508 output << "\n";
509 }
510
511 // Latency histogram buckets
512 output << "# HELP dnsdist_latency Histogram of responses by latency (in milliseconds)\n";
513 output << "# TYPE dnsdist_latency histogram\n";
514 uint64_t latency_amounts = g_stats.latency0_1;
515 output << "dnsdist_latency_bucket{le=\"1\"} " << latency_amounts << "\n";
516 latency_amounts += g_stats.latency1_10;
517 output << "dnsdist_latency_bucket{le=\"10\"} " << latency_amounts << "\n";
518 latency_amounts += g_stats.latency10_50;
519 output << "dnsdist_latency_bucket{le=\"50\"} " << latency_amounts << "\n";
520 latency_amounts += g_stats.latency50_100;
521 output << "dnsdist_latency_bucket{le=\"100\"} " << latency_amounts << "\n";
522 latency_amounts += g_stats.latency100_1000;
523 output << "dnsdist_latency_bucket{le=\"1000\"} " << latency_amounts << "\n";
524 latency_amounts += g_stats.latencySlow; // Should be the same as latency_count
525 output << "dnsdist_latency_bucket{le=\"+Inf\"} " << latency_amounts << "\n";
526 output << "dnsdist_latency_sum " << g_stats.latencySum << "\n";
527 output << "dnsdist_latency_count " << getLatencyCount(std::string()) << "\n";
528
529 auto states = g_dstates.getLocal();
530 const string statesbase = "dnsdist_server_";
531
532 output << "# HELP " << statesbase << "status " << "Whether this backend is up (1) or down (0)" << "\n";
533 output << "# TYPE " << statesbase << "status " << "gauge" << "\n";
534 output << "# HELP " << statesbase << "queries " << "Amount of queries relayed to server" << "\n";
535 output << "# TYPE " << statesbase << "queries " << "counter" << "\n";
536 output << "# HELP " << statesbase << "responses " << "Amount of responses received from this server" << "\n";
537 output << "# TYPE " << statesbase << "responses " << "counter" << "\n";
538 output << "# HELP " << statesbase << "drops " << "Amount of queries not answered by server" << "\n";
539 output << "# TYPE " << statesbase << "drops " << "counter" << "\n";
540 output << "# HELP " << statesbase << "latency " << "Server's latency when answering questions in milliseconds" << "\n";
541 output << "# TYPE " << statesbase << "latency " << "gauge" << "\n";
542 output << "# HELP " << statesbase << "senderrors " << "Total number of OS send errors while relaying queries" << "\n";
543 output << "# TYPE " << statesbase << "senderrors " << "counter" << "\n";
544 output << "# HELP " << statesbase << "outstanding " << "Current number of queries that are waiting for a backend response" << "\n";
545 output << "# TYPE " << statesbase << "outstanding " << "gauge" << "\n";
546 output << "# HELP " << statesbase << "order " << "The order in which this server is picked" << "\n";
547 output << "# TYPE " << statesbase << "order " << "gauge" << "\n";
548 output << "# HELP " << statesbase << "weight " << "The weight within the order in which this server is picked" << "\n";
549 output << "# TYPE " << statesbase << "weight " << "gauge" << "\n";
550 output << "# HELP " << statesbase << "tcpdiedsendingquery " << "The number of TCP I/O errors while sending the query" << "\n";
551 output << "# TYPE " << statesbase << "tcpdiedsendingquery " << "counter" << "\n";
552 output << "# HELP " << statesbase << "tcpdiedreadingresponse " << "The number of TCP I/O errors while reading the response" << "\n";
553 output << "# TYPE " << statesbase << "tcpdiedreadingresponse " << "counter" << "\n";
554 output << "# HELP " << statesbase << "tcpgaveup " << "The number of TCP connections failing after too many attempts" << "\n";
555 output << "# TYPE " << statesbase << "tcpgaveup " << "counter" << "\n";
556 output << "# HELP " << statesbase << "tcpreadtimeouts " << "The number of TCP read timeouts" << "\n";
557 output << "# TYPE " << statesbase << "tcpreadtimeouts " << "counter" << "\n";
558 output << "# HELP " << statesbase << "tcpwritetimeouts " << "The number of TCP write timeouts" << "\n";
559 output << "# TYPE " << statesbase << "tcpwritetimeouts " << "counter" << "\n";
560 output << "# HELP " << statesbase << "tcpcurrentconnections " << "The number of current TCP connections" << "\n";
561 output << "# TYPE " << statesbase << "tcpcurrentconnections " << "gauge" << "\n";
562 output << "# HELP " << statesbase << "tcpavgqueriesperconn " << "The average number of queries per TCP connection" << "\n";
563 output << "# TYPE " << statesbase << "tcpavgqueriesperconn " << "gauge" << "\n";
564 output << "# HELP " << statesbase << "tcpavgconnduration " << "The average duration of a TCP connection (ms)" << "\n";
565 output << "# TYPE " << statesbase << "tcpavgconnduration " << "gauge" << "\n";
566
567 for (const auto& state : *states) {
568 string serverName;
569
570 if (state->getName().empty())
571 serverName = state->remote.toStringWithPort();
572 else
573 serverName = state->getName();
574
575 boost::replace_all(serverName, ".", "_");
576
577 const std::string label = boost::str(boost::format("{server=\"%1%\",address=\"%2%\"}")
578 % serverName % state->remote.toStringWithPort());
579
580 output << statesbase << "status" << label << " " << (state->isUp() ? "1" : "0") << "\n";
581 output << statesbase << "queries" << label << " " << state->queries.load() << "\n";
582 output << statesbase << "responses" << label << " " << state->responses.load() << "\n";
583 output << statesbase << "drops" << label << " " << state->reuseds.load() << "\n";
584 output << statesbase << "latency" << label << " " << state->latencyUsec/1000.0 << "\n";
585 output << statesbase << "senderrors" << label << " " << state->sendErrors.load() << "\n";
586 output << statesbase << "outstanding" << label << " " << state->outstanding.load() << "\n";
587 output << statesbase << "order" << label << " " << state->order << "\n";
588 output << statesbase << "weight" << label << " " << state->weight << "\n";
589 output << statesbase << "tcpdiedsendingquery" << label << " " << state->tcpDiedSendingQuery << "\n";
590 output << statesbase << "tcpdiedreadingresponse" << label << " " << state->tcpDiedReadingResponse << "\n";
591 output << statesbase << "tcpgaveup" << label << " " << state->tcpGaveUp << "\n";
592 output << statesbase << "tcpreadtimeouts" << label << " " << state->tcpReadTimeouts << "\n";
593 output << statesbase << "tcpwritetimeouts" << label << " " << state->tcpWriteTimeouts << "\n";
594 output << statesbase << "tcpcurrentconnections" << label << " " << state->tcpCurrentConnections << "\n";
595 output << statesbase << "tcpavgqueriesperconn" << label << " " << state->tcpAvgQueriesPerConnection << "\n";
596 output << statesbase << "tcpavgconnduration" << label << " " << state->tcpAvgConnectionDuration << "\n";
597 }
598
599 const string frontsbase = "dnsdist_frontend_";
600 output << "# HELP " << frontsbase << "queries " << "Amount of queries received by this frontend" << "\n";
601 output << "# TYPE " << frontsbase << "queries " << "counter" << "\n";
602 output << "# HELP " << frontsbase << "responses " << "Amount of responses sent by this frontend" << "\n";
603 output << "# TYPE " << frontsbase << "responses " << "counter" << "\n";
604 output << "# HELP " << frontsbase << "tcpdiedreadingquery " << "Amount of TCP connections terminated while reading the query from the client" << "\n";
605 output << "# TYPE " << frontsbase << "tcpdiedreadingquery " << "counter" << "\n";
606 output << "# HELP " << frontsbase << "tcpdiedsendingresponse " << "Amount of TCP connections terminated while sending a response to the client" << "\n";
607 output << "# TYPE " << frontsbase << "tcpdiedsendingresponse " << "counter" << "\n";
608 output << "# HELP " << frontsbase << "tcpgaveup " << "Amount of TCP connections terminated after too many attempts to get a connection to the backend" << "\n";
609 output << "# TYPE " << frontsbase << "tcpgaveup " << "counter" << "\n";
610 output << "# HELP " << frontsbase << "tcpclientimeouts " << "Amount of TCP connections terminated by a timeout while reading from the client" << "\n";
611 output << "# TYPE " << frontsbase << "tcpclientimeouts " << "counter" << "\n";
612 output << "# HELP " << frontsbase << "tcpdownstreamtimeouts " << "Amount of TCP connections terminated by a timeout while reading from the backend" << "\n";
613 output << "# TYPE " << frontsbase << "tcpdownstreamtimeouts " << "counter" << "\n";
614 output << "# HELP " << frontsbase << "tcpcurrentconnections " << "Amount of current incoming TCP connections from clients" << "\n";
615 output << "# TYPE " << frontsbase << "tcpcurrentconnections " << "gauge" << "\n";
616 output << "# HELP " << frontsbase << "tcpavgqueriesperconnection " << "The average number of queries per TCP connection" << "\n";
617 output << "# TYPE " << frontsbase << "tcpavgqueriesperconnection " << "gauge" << "\n";
618 output << "# HELP " << frontsbase << "tcpavgconnectionduration " << "The average duration of a TCP connection (ms)" << "\n";
619 output << "# TYPE " << frontsbase << "tcpavgconnectionduration " << "gauge" << "\n";
620 output << "# HELP " << frontsbase << "tlsqueries " << "Number of queries received by dnsdist over TLS, by TLS version" << "\n";
621 output << "# TYPE " << frontsbase << "tlsqueries " << "counter" << "\n";
622 output << "# HELP " << frontsbase << "tlsnewsessions " << "Amount of new TLS sessions negotiated" << "\n";
623 output << "# TYPE " << frontsbase << "tlsnewsessions " << "counter" << "\n";
624 output << "# HELP " << frontsbase << "tlsresumptions " << "Amount of TLS sessions resumed" << "\n";
625 output << "# TYPE " << frontsbase << "tlsresumptions " << "counter" << "\n";
626 output << "# HELP " << frontsbase << "tlsunknownticketkeys " << "Amount of attempts to resume TLS session from an unknown key (possibly expired)" << "\n";
627 output << "# TYPE " << frontsbase << "tlsunknownticketkeys " << "counter" << "\n";
628 output << "# HELP " << frontsbase << "tlsinactiveticketkeys " << "Amount of TLS sessions resumed from an inactive key" << "\n";
629 output << "# TYPE " << frontsbase << "tlsinactiveticketkeys " << "counter" << "\n";
630
631 output << "# HELP " << frontsbase << "tlshandshakefailures " << "Amount of TLS handshake failures" << "\n";
632 output << "# TYPE " << frontsbase << "tlshandshakefailures " << "counter" << "\n";
633
634 std::map<std::string,uint64_t> frontendDuplicates;
635 for (const auto& front : g_frontends) {
636 if (front->udpFD == -1 && front->tcpFD == -1)
637 continue;
638
639 const string frontName = front->local.toString() + ":" + std::to_string(front->local.getPort());
640 const string proto = front->getType();
641 const string fullName = frontName + "_" + proto;
642 uint64_t threadNumber = 0;
643 auto dupPair = frontendDuplicates.insert({fullName, 1});
644 if (!dupPair.second) {
645 threadNumber = dupPair.first->second;
646 ++(dupPair.first->second);
647 }
648 const std::string label = boost::str(boost::format("{frontend=\"%1%\",proto=\"%2%\",thread=\"%3%\"} ")
649 % frontName % proto % threadNumber);
650
651 output << frontsbase << "queries" << label << front->queries.load() << "\n";
652 output << frontsbase << "responses" << label << front->responses.load() << "\n";
653 if (front->isTCP()) {
654 output << frontsbase << "tcpdiedreadingquery" << label << front->tcpDiedReadingQuery.load() << "\n";
655 output << frontsbase << "tcpdiedsendingresponse" << label << front->tcpDiedSendingResponse.load() << "\n";
656 output << frontsbase << "tcpgaveup" << label << front->tcpGaveUp.load() << "\n";
657 output << frontsbase << "tcpclientimeouts" << label << front->tcpClientTimeouts.load() << "\n";
658 output << frontsbase << "tcpdownstreamtimeouts" << label << front->tcpDownstreamTimeouts.load() << "\n";
659 output << frontsbase << "tcpcurrentconnections" << label << front->tcpCurrentConnections.load() << "\n";
660 output << frontsbase << "tcpavgqueriesperconnection" << label << front->tcpAvgQueriesPerConnection.load() << "\n";
661 output << frontsbase << "tcpavgconnectionduration" << label << front->tcpAvgConnectionDuration.load() << "\n";
662 if (front->hasTLS()) {
663 output << frontsbase << "tlsnewsessions" << label << front->tlsNewSessions.load() << "\n";
664 output << frontsbase << "tlsresumptions" << label << front->tlsResumptions.load() << "\n";
665 output << frontsbase << "tlsunknownticketkeys" << label << front->tlsUnknownTicketKey.load() << "\n";
666 output << frontsbase << "tlsinactiveticketkeys" << label << front->tlsInactiveTicketKey.load() << "\n";
667
668 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls10\"} " << front->tls10queries.load() << "\n";
669 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls11\"} " << front->tls11queries.load() << "\n";
670 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls12\"} " << front->tls12queries.load() << "\n";
671 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"tls13\"} " << front->tls13queries.load() << "\n";
672 output << frontsbase << "tlsqueries{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",tls=\"unknown\"} " << front->tlsUnknownqueries.load() << "\n";
673
674 const TLSErrorCounters* errorCounters = nullptr;
675 if (front->tlsFrontend != nullptr) {
676 errorCounters = &front->tlsFrontend->d_tlsCounters;
677 }
678 else if (front->dohFrontend != nullptr) {
679 errorCounters = &front->dohFrontend->d_tlsCounters;
680 }
681
682 if (errorCounters != nullptr) {
683 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"dhKeyTooSmall\"} " << errorCounters->d_dhKeyTooSmall << "\n";
684 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"inappropriateFallBack\"} " << errorCounters->d_inappropriateFallBack << "\n";
685 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"noSharedCipher\"} " << errorCounters->d_noSharedCipher << "\n";
686 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownCipherType\"} " << errorCounters->d_unknownCipherType << "\n";
687 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownKeyExchangeType\"} " << errorCounters->d_unknownKeyExchangeType << "\n";
688 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unknownProtocol\"} " << errorCounters->d_unknownProtocol << "\n";
689 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedEC\"} " << errorCounters->d_unsupportedEC << "\n";
690 output << frontsbase << "tlshandshakefailures{frontend=\"" << frontName << "\",proto=\"" << proto << "\",thread=\"" << threadNumber << "\",error=\"unsupportedProtocol{\"} " << errorCounters->d_unsupportedProtocol << "\n";
691 }
692 }
693 }
694 }
695
696 output << "# HELP " << frontsbase << "http_connects " << "Number of DoH TCP connections established to this frontend" << "\n";
697 output << "# TYPE " << frontsbase << "http_connects " << "counter" << "\n";
698
699 output << "# HELP " << frontsbase << "doh_http_method_queries " << "Number of DoH queries received by dnsdist, by HTTP method" << "\n";
700 output << "# TYPE " << frontsbase << "doh_http_method_queries " << "counter" << "\n";
701
702 output << "# HELP " << frontsbase << "doh_http_version_queries " << "Number of DoH queries received by dnsdist, by HTTP version" << "\n";
703 output << "# TYPE " << frontsbase << "doh_http_version_queries " << "counter" << "\n";
704
705 output << "# HELP " << frontsbase << "doh_bad_requests " << "Number of requests that could not be converted to a DNS query" << "\n";
706 output << "# TYPE " << frontsbase << "doh_bad_requests " << "counter" << "\n";
707
708 output << "# HELP " << frontsbase << "doh_responses " << "Number of responses sent, by type" << "\n";
709 output << "# TYPE " << frontsbase << "doh_responses " << "counter" << "\n";
710
711 output << "# HELP " << frontsbase << "doh_version_status_responses " << "Number of requests that could not be converted to a DNS query" << "\n";
712 output << "# TYPE " << frontsbase << "doh_version_status_responses " << "counter" << "\n";
713
714 #ifdef HAVE_DNS_OVER_HTTPS
715 std::map<std::string,uint64_t> dohFrontendDuplicates;
716 for(const auto& doh : g_dohlocals) {
717 const string frontName = doh->d_local.toStringWithPort();
718 uint64_t threadNumber = 0;
719 auto dupPair = frontendDuplicates.insert({frontName, 1});
720 if (!dupPair.second) {
721 threadNumber = dupPair.first->second;
722 ++(dupPair.first->second);
723 }
724 const std::string addrlabel = boost::str(boost::format("frontend=\"%1%\",thread=\"%2%\"") % frontName % threadNumber);
725 const std::string label = "{" + addrlabel + "} ";
726
727 output << frontsbase << "http_connects" << label << doh->d_httpconnects << "\n";
728 output << frontsbase << "doh_http_method_queries{method=\"get\"," << addrlabel << "} " << doh->d_getqueries << "\n";
729 output << frontsbase << "doh_http_method_queries{method=\"post\"," << addrlabel << "} " << doh->d_postqueries << "\n";
730
731 output << frontsbase << "doh_http_version_queries{version=\"1\"," << addrlabel << "} " << doh->d_http1Stats.d_nbQueries << "\n";
732 output << frontsbase << "doh_http_version_queries{version=\"2\"," << addrlabel << "} " << doh->d_http2Stats.d_nbQueries << "\n";
733
734 output << frontsbase << "doh_bad_requests{" << addrlabel << "} " << doh->d_badrequests << "\n";
735
736 output << frontsbase << "doh_responses{type=\"error\"," << addrlabel << "} " << doh->d_errorresponses << "\n";
737 output << frontsbase << "doh_responses{type=\"redirect\"," << addrlabel << "} " << doh->d_redirectresponses << "\n";
738 output << frontsbase << "doh_responses{type=\"valid\"," << addrlabel << "} " << doh->d_validresponses << "\n";
739
740 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"200\"," << addrlabel << "} " << doh->d_http1Stats.d_nb200Responses << "\n";
741 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"400\"," << addrlabel << "} " << doh->d_http1Stats.d_nb400Responses << "\n";
742 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"403\"," << addrlabel << "} " << doh->d_http1Stats.d_nb403Responses << "\n";
743 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"500\"," << addrlabel << "} " << doh->d_http1Stats.d_nb500Responses << "\n";
744 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"502\"," << addrlabel << "} " << doh->d_http1Stats.d_nb502Responses << "\n";
745 output << frontsbase << "doh_version_status_responses{httpversion=\"1\",status=\"other\"," << addrlabel << "} " << doh->d_http1Stats.d_nbOtherResponses << "\n";
746 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"200\"," << addrlabel << "} " << doh->d_http2Stats.d_nb200Responses << "\n";
747 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"400\"," << addrlabel << "} " << doh->d_http2Stats.d_nb400Responses << "\n";
748 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"403\"," << addrlabel << "} " << doh->d_http2Stats.d_nb403Responses << "\n";
749 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"500\"," << addrlabel << "} " << doh->d_http2Stats.d_nb500Responses << "\n";
750 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"502\"," << addrlabel << "} " << doh->d_http2Stats.d_nb502Responses << "\n";
751 output << frontsbase << "doh_version_status_responses{httpversion=\"2\",status=\"other\"," << addrlabel << "} " << doh->d_http2Stats.d_nbOtherResponses << "\n";
752 }
753 #endif /* HAVE_DNS_OVER_HTTPS */
754
755 auto localPools = g_pools.getLocal();
756 const string cachebase = "dnsdist_pool_";
757 output << "# HELP dnsdist_pool_servers " << "Number of servers in that pool" << "\n";
758 output << "# TYPE dnsdist_pool_servers " << "gauge" << "\n";
759 output << "# HELP dnsdist_pool_active_servers " << "Number of available servers in that pool" << "\n";
760 output << "# TYPE dnsdist_pool_active_servers " << "gauge" << "\n";
761
762 output << "# HELP dnsdist_pool_cache_size " << "Maximum number of entries that this cache can hold" << "\n";
763 output << "# TYPE dnsdist_pool_cache_size " << "gauge" << "\n";
764 output << "# HELP dnsdist_pool_cache_entries " << "Number of entries currently present in that cache" << "\n";
765 output << "# TYPE dnsdist_pool_cache_entries " << "gauge" << "\n";
766 output << "# HELP dnsdist_pool_cache_hits " << "Number of hits from that cache" << "\n";
767 output << "# TYPE dnsdist_pool_cache_hits " << "counter" << "\n";
768 output << "# HELP dnsdist_pool_cache_misses " << "Number of misses from that cache" << "\n";
769 output << "# TYPE dnsdist_pool_cache_misses " << "counter" << "\n";
770 output << "# HELP dnsdist_pool_cache_deferred_inserts " << "Number of insertions into that cache skipped because it was already locked" << "\n";
771 output << "# TYPE dnsdist_pool_cache_deferred_inserts " << "counter" << "\n";
772 output << "# HELP dnsdist_pool_cache_deferred_lookups " << "Number of lookups into that cache skipped because it was already locked" << "\n";
773 output << "# TYPE dnsdist_pool_cache_deferred_lookups " << "counter" << "\n";
774 output << "# HELP dnsdist_pool_cache_lookup_collisions " << "Number of lookups into that cache that triggered a collision (same hash but different entry)" << "\n";
775 output << "# TYPE dnsdist_pool_cache_lookup_collisions " << "counter" << "\n";
776 output << "# HELP dnsdist_pool_cache_insert_collisions " << "Number of insertions into that cache that triggered a collision (same hash but different entry)" << "\n";
777 output << "# TYPE dnsdist_pool_cache_insert_collisions " << "counter" << "\n";
778 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";
779 output << "# TYPE dnsdist_pool_cache_ttl_too_shorts " << "counter" << "\n";
780
781 for (const auto& entry : *localPools) {
782 string poolName = entry.first;
783
784 if (poolName.empty()) {
785 poolName = "_default_";
786 }
787 const string label = "{pool=\"" + poolName + "\"}";
788 const std::shared_ptr<ServerPool> pool = entry.second;
789 output << "dnsdist_pool_servers" << label << " " << pool->countServers(false) << "\n";
790 output << "dnsdist_pool_active_servers" << label << " " << pool->countServers(true) << "\n";
791
792 if (pool->packetCache != nullptr) {
793 const auto& cache = pool->packetCache;
794
795 output << cachebase << "cache_size" <<label << " " << cache->getMaxEntries() << "\n";
796 output << cachebase << "cache_entries" <<label << " " << cache->getEntriesCount() << "\n";
797 output << cachebase << "cache_hits" <<label << " " << cache->getHits() << "\n";
798 output << cachebase << "cache_misses" <<label << " " << cache->getMisses() << "\n";
799 output << cachebase << "cache_deferred_inserts" <<label << " " << cache->getDeferredInserts() << "\n";
800 output << cachebase << "cache_deferred_lookups" <<label << " " << cache->getDeferredLookups() << "\n";
801 output << cachebase << "cache_lookup_collisions" <<label << " " << cache->getLookupCollisions() << "\n";
802 output << cachebase << "cache_insert_collisions" <<label << " " << cache->getInsertCollisions() << "\n";
803 output << cachebase << "cache_ttl_too_shorts" <<label << " " << cache->getTTLTooShorts() << "\n";
804 }
805 }
806
807 resp.body = output.str();
808 resp.headers["Content-Type"] = "text/plain";
809 }
810
811 else if(req.url.path=="/api/v1/servers/localhost") {
812 handleCORS(req, resp);
813 resp.status=200;
814
815 Json::array servers;
816 auto localServers = g_dstates.getLocal();
817 int num=0;
818 for(const auto& a : *localServers) {
819 string status;
820 if(a->availability == DownstreamState::Availability::Up)
821 status = "UP";
822 else if(a->availability == DownstreamState::Availability::Down)
823 status = "DOWN";
824 else
825 status = (a->upStatus ? "up" : "down");
826
827 Json::array pools;
828 for(const auto& p: a->pools)
829 pools.push_back(p);
830
831 Json::object server{
832 {"id", num++},
833 {"name", a->getName()},
834 {"address", a->remote.toStringWithPort()},
835 {"state", status},
836 {"qps", (double)a->queryLoad},
837 {"qpsLimit", (double)a->qps.getRate()},
838 {"outstanding", (double)a->outstanding},
839 {"reuseds", (double)a->reuseds},
840 {"weight", (double)a->weight},
841 {"order", (double)a->order},
842 {"pools", pools},
843 {"latency", (double)(a->latencyUsec/1000.0)},
844 {"queries", (double)a->queries},
845 {"responses", (double)a->responses},
846 {"sendErrors", (double)a->sendErrors},
847 {"tcpDiedSendingQuery", (double)a->tcpDiedSendingQuery},
848 {"tcpDiedReadingResponse", (double)a->tcpDiedReadingResponse},
849 {"tcpGaveUp", (double)a->tcpGaveUp},
850 {"tcpReadTimeouts", (double)a->tcpReadTimeouts},
851 {"tcpWriteTimeouts", (double)a->tcpWriteTimeouts},
852 {"tcpCurrentConnections", (double)a->tcpCurrentConnections},
853 {"tcpAvgQueriesPerConnection", (double)a->tcpAvgQueriesPerConnection},
854 {"tcpAvgConnectionDuration", (double)a->tcpAvgConnectionDuration},
855 {"dropRate", (double)a->dropRate}
856 };
857
858 /* sending a latency for a DOWN server doesn't make sense */
859 if (a->availability == DownstreamState::Availability::Down) {
860 server["latency"] = nullptr;
861 }
862
863 servers.push_back(server);
864 }
865
866 Json::array frontends;
867 num=0;
868 for(const auto& front : g_frontends) {
869 if (front->udpFD == -1 && front->tcpFD == -1)
870 continue;
871 Json::object frontend{
872 { "id", num++ },
873 { "address", front->local.toStringWithPort() },
874 { "udp", front->udpFD >= 0 },
875 { "tcp", front->tcpFD >= 0 },
876 { "type", front->getType() },
877 { "queries", (double) front->queries.load() },
878 { "responses", (double) front->responses.load() },
879 { "tcpDiedReadingQuery", (double) front->tcpDiedReadingQuery.load() },
880 { "tcpDiedSendingResponse", (double) front->tcpDiedSendingResponse.load() },
881 { "tcpGaveUp", (double) front->tcpGaveUp.load() },
882 { "tcpClientTimeouts", (double) front->tcpClientTimeouts },
883 { "tcpDownstreamTimeouts", (double) front->tcpDownstreamTimeouts },
884 { "tcpCurrentConnections", (double) front->tcpCurrentConnections },
885 { "tcpAvgQueriesPerConnection", (double) front->tcpAvgQueriesPerConnection },
886 { "tcpAvgConnectionDuration", (double) front->tcpAvgConnectionDuration },
887 { "tlsNewSessions", (double) front->tlsNewSessions },
888 { "tlsResumptions", (double) front->tlsResumptions },
889 { "tlsUnknownTicketKey", (double) front->tlsUnknownTicketKey },
890 { "tlsInactiveTicketKey", (double) front->tlsInactiveTicketKey },
891 { "tls10Queries", (double) front->tls10queries },
892 { "tls11Queries", (double) front->tls11queries },
893 { "tls12Queries", (double) front->tls12queries },
894 { "tls13Queries", (double) front->tls13queries },
895 { "tlsUnknownQueries", (double) front->tlsUnknownqueries },
896 };
897 const TLSErrorCounters* errorCounters = nullptr;
898 if (front->tlsFrontend != nullptr) {
899 errorCounters = &front->tlsFrontend->d_tlsCounters;
900 }
901 else if (front->dohFrontend != nullptr) {
902 errorCounters = &front->dohFrontend->d_tlsCounters;
903 }
904 if (errorCounters != nullptr) {
905 frontend["tlsHandshakeFailuresDHKeyTooSmall"] = (double)errorCounters->d_dhKeyTooSmall;
906 frontend["tlsHandshakeFailuresInappropriateFallBack"] = (double)errorCounters->d_inappropriateFallBack;
907 frontend["tlsHandshakeFailuresNoSharedCipher"] = (double)errorCounters->d_noSharedCipher;
908 frontend["tlsHandshakeFailuresUnknownCipher"] = (double)errorCounters->d_unknownCipherType;
909 frontend["tlsHandshakeFailuresUnknownKeyExchangeType"] = (double)errorCounters->d_unknownKeyExchangeType;
910 frontend["tlsHandshakeFailuresUnknownProtocol"] = (double)errorCounters->d_unknownProtocol;
911 frontend["tlsHandshakeFailuresUnsupportedEC"] = (double)errorCounters->d_unsupportedEC;
912 frontend["tlsHandshakeFailuresUnsupportedProtocol"] = (double)errorCounters->d_unsupportedProtocol;
913 }
914 frontends.push_back(frontend);
915 }
916
917 Json::array dohs;
918 #ifdef HAVE_DNS_OVER_HTTPS
919 {
920 num = 0;
921 for(const auto& doh : g_dohlocals) {
922 Json::object obj{
923 { "id", num++ },
924 { "address", doh->d_local.toStringWithPort() },
925 { "http-connects", (double) doh->d_httpconnects },
926 { "http1-queries", (double) doh->d_http1Stats.d_nbQueries },
927 { "http2-queries", (double) doh->d_http2Stats.d_nbQueries },
928 { "http1-200-responses", (double) doh->d_http1Stats.d_nb200Responses },
929 { "http2-200-responses", (double) doh->d_http2Stats.d_nb200Responses },
930 { "http1-400-responses", (double) doh->d_http1Stats.d_nb400Responses },
931 { "http2-400-responses", (double) doh->d_http2Stats.d_nb400Responses },
932 { "http1-403-responses", (double) doh->d_http1Stats.d_nb403Responses },
933 { "http2-403-responses", (double) doh->d_http2Stats.d_nb403Responses },
934 { "http1-500-responses", (double) doh->d_http1Stats.d_nb500Responses },
935 { "http2-500-responses", (double) doh->d_http2Stats.d_nb500Responses },
936 { "http1-502-responses", (double) doh->d_http1Stats.d_nb502Responses },
937 { "http2-502-responses", (double) doh->d_http2Stats.d_nb502Responses },
938 { "http1-other-responses", (double) doh->d_http1Stats.d_nbOtherResponses },
939 { "http2-other-responses", (double) doh->d_http2Stats.d_nbOtherResponses },
940 { "get-queries", (double) doh->d_getqueries },
941 { "post-queries", (double) doh->d_postqueries },
942 { "bad-requests", (double) doh->d_badrequests },
943 { "error-responses", (double) doh->d_errorresponses },
944 { "redirect-responses", (double) doh->d_redirectresponses },
945 { "valid-responses", (double) doh->d_validresponses }
946 };
947 dohs.push_back(obj);
948 }
949 }
950 #endif /* HAVE_DNS_OVER_HTTPS */
951
952 Json::array pools;
953 auto localPools = g_pools.getLocal();
954 num=0;
955 for(const auto& pool : *localPools) {
956 const auto& cache = pool.second->packetCache;
957 Json::object entry {
958 { "id", num++ },
959 { "name", pool.first },
960 { "serversCount", (double) pool.second->countServers(false) },
961 { "cacheSize", (double) (cache ? cache->getMaxEntries() : 0) },
962 { "cacheEntries", (double) (cache ? cache->getEntriesCount() : 0) },
963 { "cacheHits", (double) (cache ? cache->getHits() : 0) },
964 { "cacheMisses", (double) (cache ? cache->getMisses() : 0) },
965 { "cacheDeferredInserts", (double) (cache ? cache->getDeferredInserts() : 0) },
966 { "cacheDeferredLookups", (double) (cache ? cache->getDeferredLookups() : 0) },
967 { "cacheLookupCollisions", (double) (cache ? cache->getLookupCollisions() : 0) },
968 { "cacheInsertCollisions", (double) (cache ? cache->getInsertCollisions() : 0) },
969 { "cacheTTLTooShorts", (double) (cache ? cache->getTTLTooShorts() : 0) }
970 };
971 pools.push_back(entry);
972 }
973
974 Json::array rules;
975 auto localRules = g_rulactions.getLocal();
976 num=0;
977 for(const auto& a : *localRules) {
978 Json::object rule{
979 {"id", num++},
980 {"creationOrder", (double)a.d_creationOrder},
981 {"uuid", boost::uuids::to_string(a.d_id)},
982 {"matches", (double)a.d_rule->d_matches},
983 {"rule", a.d_rule->toString()},
984 {"action", a.d_action->toString()},
985 {"action-stats", a.d_action->getStats()}
986 };
987 rules.push_back(rule);
988 }
989
990 auto responseRules = someResponseRulesToJson(&g_resprulactions);
991 auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitresprulactions);
992 auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredresprulactions);
993
994 string acl;
995
996 vector<string> vec;
997 g_ACL.getLocal()->toStringVector(&vec);
998
999 for(const auto& s : vec) {
1000 if(!acl.empty()) acl += ", ";
1001 acl+=s;
1002 }
1003 string localaddressesStr;
1004 std::set<std::string> localaddresses;
1005 for(const auto& front : g_frontends) {
1006 localaddresses.insert(front->local.toStringWithPort());
1007 }
1008 for (const auto& addr : localaddresses) {
1009 if (!localaddressesStr.empty()) {
1010 localaddressesStr += ", ";
1011 }
1012 localaddressesStr += addr;
1013 }
1014
1015 Json my_json = Json::object {
1016 { "daemon_type", "dnsdist" },
1017 { "version", VERSION},
1018 { "servers", servers},
1019 { "frontends", frontends },
1020 { "pools", pools },
1021 { "rules", rules},
1022 { "response-rules", responseRules},
1023 { "cache-hit-response-rules", cacheHitResponseRules},
1024 { "self-answered-response-rules", selfAnsweredResponseRules},
1025 { "acl", acl},
1026 { "local", localaddressesStr},
1027 { "dohFrontends", dohs }
1028 };
1029 resp.headers["Content-Type"] = "application/json";
1030 resp.body=my_json.dump();
1031 }
1032 else if(req.url.path=="/api/v1/servers/localhost/statistics") {
1033 handleCORS(req, resp);
1034 resp.status=200;
1035
1036 Json::array doc;
1037 for(const auto& item : g_stats.entries) {
1038 if (item.first == "special-memory-usage")
1039 continue; // Too expensive for get-all
1040
1041 if(const auto& val = boost::get<DNSDistStats::stat_t*>(&item.second)) {
1042 doc.push_back(Json::object {
1043 { "type", "StatisticItem" },
1044 { "name", item.first },
1045 { "value", (double)(*val)->load() }
1046 });
1047 }
1048 else if (const auto& dval = boost::get<double*>(&item.second)) {
1049 doc.push_back(Json::object {
1050 { "type", "StatisticItem" },
1051 { "name", item.first },
1052 { "value", (**dval) }
1053 });
1054 }
1055 else {
1056 doc.push_back(Json::object {
1057 { "type", "StatisticItem" },
1058 { "name", item.first },
1059 { "value", (double)(*boost::get<DNSDistStats::statfunction_t>(&item.second))(item.first) }
1060 });
1061 }
1062 }
1063 Json my_json = doc;
1064 resp.body=my_json.dump();
1065 resp.headers["Content-Type"] = "application/json";
1066 }
1067 else if(req.url.path=="/api/v1/servers/localhost/config") {
1068 handleCORS(req, resp);
1069 resp.status=200;
1070
1071 Json::array doc;
1072 typedef boost::variant<bool, double, std::string> configentry_t;
1073 std::vector<std::pair<std::string, configentry_t> > configEntries {
1074 { "acl", g_ACL.getLocal()->toString() },
1075 { "allow-empty-response", g_allowEmptyResponse },
1076 { "control-socket", g_serverControl.toStringWithPort() },
1077 { "ecs-override", g_ECSOverride },
1078 { "ecs-source-prefix-v4", (double) g_ECSSourcePrefixV4 },
1079 { "ecs-source-prefix-v6", (double) g_ECSSourcePrefixV6 },
1080 { "fixup-case", g_fixupCase },
1081 { "max-outstanding", (double) g_maxOutstanding },
1082 { "server-policy", g_policy.getLocal()->name },
1083 { "stale-cache-entries-ttl", (double) g_staleCacheEntriesTTL },
1084 { "tcp-recv-timeout", (double) g_tcpRecvTimeout },
1085 { "tcp-send-timeout", (double) g_tcpSendTimeout },
1086 { "truncate-tc", g_truncateTC },
1087 { "verbose", g_verbose },
1088 { "verbose-health-checks", g_verboseHealthChecks }
1089 };
1090 for(const auto& item : configEntries) {
1091 if (const auto& bval = boost::get<bool>(&item.second)) {
1092 doc.push_back(Json::object {
1093 { "type", "ConfigSetting" },
1094 { "name", item.first },
1095 { "value", *bval }
1096 });
1097 }
1098 else if (const auto& sval = boost::get<string>(&item.second)) {
1099 doc.push_back(Json::object {
1100 { "type", "ConfigSetting" },
1101 { "name", item.first },
1102 { "value", *sval }
1103 });
1104 }
1105 else if (const auto& dval = boost::get<double>(&item.second)) {
1106 doc.push_back(Json::object {
1107 { "type", "ConfigSetting" },
1108 { "name", item.first },
1109 { "value", *dval }
1110 });
1111 }
1112 }
1113 Json my_json = doc;
1114 resp.body=my_json.dump();
1115 resp.headers["Content-Type"] = "application/json";
1116 }
1117 else if(req.url.path=="/api/v1/servers/localhost/config/allow-from") {
1118 handleCORS(req, resp);
1119
1120 resp.headers["Content-Type"] = "application/json";
1121 resp.status=200;
1122
1123 if (req.method == "PUT") {
1124 std::string err;
1125 Json doc = Json::parse(req.body, err);
1126
1127 if (!doc.is_null()) {
1128 NetmaskGroup nmg;
1129 auto aclList = doc["value"];
1130 if (aclList.is_array()) {
1131
1132 for (auto value : aclList.array_items()) {
1133 try {
1134 nmg.addMask(value.string_value());
1135 } catch (NetmaskException &e) {
1136 resp.status = 400;
1137 break;
1138 }
1139 }
1140
1141 if (resp.status == 200) {
1142 infolog("Updating the ACL via the API to %s", nmg.toString());
1143 g_ACL.setState(nmg);
1144 apiSaveACL(nmg);
1145 }
1146 }
1147 else {
1148 resp.status = 400;
1149 }
1150 }
1151 else {
1152 resp.status = 400;
1153 }
1154 }
1155 if (resp.status == 200) {
1156 Json::array acl;
1157 vector<string> vec;
1158 g_ACL.getLocal()->toStringVector(&vec);
1159
1160 for(const auto& s : vec) {
1161 acl.push_back(s);
1162 }
1163
1164 Json::object obj{
1165 { "type", "ConfigSetting" },
1166 { "name", "allow-from" },
1167 { "value", acl }
1168 };
1169 Json my_json = obj;
1170 resp.body=my_json.dump();
1171 }
1172 }
1173 else if(!req.url.path.empty() && g_urlmap.count(req.url.path.c_str()+1)) {
1174 resp.body.assign(g_urlmap[req.url.path.c_str()+1]);
1175 vector<string> parts;
1176 stringtok(parts, req.url.path, ".");
1177 if(parts.back() == "html")
1178 resp.headers["Content-Type"] = "text/html" + charset;
1179 else if(parts.back() == "css")
1180 resp.headers["Content-Type"] = "text/css" + charset;
1181 else if(parts.back() == "js")
1182 resp.headers["Content-Type"] = "application/javascript" + charset;
1183 else if(parts.back() == "png")
1184 resp.headers["Content-Type"] = "image/png";
1185 resp.status=200;
1186 }
1187 else if(req.url.path=="/") {
1188 resp.body.assign(g_urlmap["index.html"]);
1189 resp.headers["Content-Type"] = "text/html" + charset;
1190 resp.status=200;
1191 }
1192 else {
1193 // cerr<<"404 for: "<<req.url.path<<endl;
1194 resp.status=404;
1195 }
1196
1197 std::ostringstream ofs;
1198 ofs << resp;
1199 string done;
1200 done=ofs.str();
1201 writen2(sock, done.c_str(), done.size());
1202
1203 close(sock);
1204 sock = -1;
1205 }
1206 catch(const YaHTTP::ParseError& e) {
1207 vinfolog("Webserver thread died with parse error exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
1208 close(sock);
1209 }
1210 catch(const std::exception& e) {
1211 errlog("Webserver thread died with exception while processing a request from %s: %s", remote.toStringWithPort(), e.what());
1212 close(sock);
1213 }
1214 catch(...) {
1215 errlog("Webserver thread died with exception while processing a request from %s", remote.toStringWithPort());
1216 close(sock);
1217 }
1218 }
1219
1220 void setWebserverAPIKey(const boost::optional<std::string> apiKey)
1221 {
1222 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
1223
1224 if (apiKey) {
1225 g_webserverConfig.apiKey = *apiKey;
1226 } else {
1227 g_webserverConfig.apiKey.clear();
1228 }
1229 }
1230
1231 void setWebserverPassword(const std::string& password)
1232 {
1233 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
1234
1235 g_webserverConfig.password = password;
1236 }
1237
1238 void setWebserverACL(const std::string& acl)
1239 {
1240 NetmaskGroup newACL;
1241 newACL.toMasks(acl);
1242
1243 {
1244 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
1245 g_webserverConfig.acl = std::move(newACL);
1246 }
1247 }
1248
1249 void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders)
1250 {
1251 std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
1252
1253 g_webserverConfig.customHeaders = customHeaders;
1254 }
1255
1256 void dnsdistWebserverThread(int sock, const ComboAddress& local)
1257 {
1258 setThreadName("dnsdist/webserv");
1259 warnlog("Webserver launched on %s", local.toStringWithPort());
1260
1261 for(;;) {
1262 try {
1263 ComboAddress remote(local);
1264 int fd = SAccept(sock, remote);
1265 if (!isClientAllowedByACL(remote)) {
1266 vinfolog("Connection to webserver from client %s is not allowed, closing", remote.toStringWithPort());
1267 close(fd);
1268 continue;
1269 }
1270 vinfolog("Got a connection to the webserver from %s", remote.toStringWithPort());
1271 std::thread t(connectionThread, fd, remote);
1272 t.detach();
1273 }
1274 catch (const std::exception& e) {
1275 errlog("Had an error accepting new webserver connection: %s", e.what());
1276 }
1277 }
1278 }