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