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