]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/dnsdist-dynblocks.hh
Merge pull request #14021 from Habbie/auth-lua-join-whitespace
[thirdparty/pdns.git] / pdns / dnsdist-dynblocks.hh
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 #pragma once
23
24 #include <unordered_set>
25
26 #include "dolog.hh"
27 #include "dnsdist-rings.hh"
28 #include "statnode.hh"
29
30 #include "dnsdist-lua-inspection-ffi.hh"
31
32 // dnsdist_ffi_stat_node_t is a lightuserdata
33 template<>
34 struct LuaContext::Pusher<dnsdist_ffi_stat_node_t*> {
35 static const int minSize = 1;
36 static const int maxSize = 1;
37
38 static PushedObject push(lua_State* state, dnsdist_ffi_stat_node_t* ptr) noexcept {
39 lua_pushlightuserdata(state, ptr);
40 return PushedObject{state, 1};
41 }
42 };
43
44 typedef std::function<bool(dnsdist_ffi_stat_node_t*)> dnsdist_ffi_stat_node_visitor_t;
45
46 struct dnsdist_ffi_stat_node_t
47 {
48 dnsdist_ffi_stat_node_t(const StatNode& node_, const StatNode::Stat& self_, const StatNode::Stat& children_): node(node_), self(self_), children(children_)
49 {
50 }
51
52 const StatNode& node;
53 const StatNode::Stat& self;
54 const StatNode::Stat& children;
55 };
56
57 class DynBlockRulesGroup
58 {
59 private:
60
61 struct Counts
62 {
63 std::map<uint8_t, uint64_t> d_rcodeCounts;
64 std::map<uint16_t, uint64_t> d_qtypeCounts;
65 uint64_t queries{0};
66 uint64_t respBytes{0};
67 };
68
69 struct DynBlockRule
70 {
71 DynBlockRule(): d_enabled(false)
72 {
73 }
74
75 DynBlockRule(const std::string& blockReason, unsigned int blockDuration, unsigned int rate, unsigned int warningRate, unsigned int seconds, DNSAction::Action action): d_blockReason(blockReason), d_blockDuration(blockDuration), d_rate(rate), d_warningRate(warningRate), d_seconds(seconds), d_action(action), d_enabled(true)
76 {
77 }
78
79 bool matches(const struct timespec& when)
80 {
81 if (!d_enabled) {
82 return false;
83 }
84
85 if (d_seconds && when < d_cutOff) {
86 return false;
87 }
88
89 if (when < d_minTime) {
90 d_minTime = when;
91 }
92
93 return true;
94 }
95
96 bool rateExceeded(unsigned int count, const struct timespec& now) const
97 {
98 if (!d_enabled) {
99 return false;
100 }
101
102 double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime);
103 double limit = delta * d_rate;
104 return (count > limit);
105 }
106
107 bool warningRateExceeded(unsigned int count, const struct timespec& now) const
108 {
109 if (d_warningRate == 0) {
110 return false;
111 }
112
113 double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime);
114 double limit = delta * d_warningRate;
115 return (count > limit);
116 }
117
118 bool isEnabled() const
119 {
120 return d_enabled;
121 }
122
123 std::string toString() const
124 {
125 if (!isEnabled()) {
126 return "";
127 }
128
129 std::stringstream result;
130 if (d_action != DNSAction::Action::None) {
131 result << DNSAction::typeToString(d_action) << " ";
132 }
133 else {
134 result << "Apply the global DynBlock action ";
135 }
136 result << "for " << std::to_string(d_blockDuration) << " seconds when over " << std::to_string(d_rate) << " during the last " << d_seconds << " seconds, reason: '" << d_blockReason << "'";
137
138 return result.str();
139 }
140
141 std::string d_blockReason;
142 struct timespec d_cutOff;
143 struct timespec d_minTime;
144 unsigned int d_blockDuration{0};
145 unsigned int d_rate{0};
146 unsigned int d_warningRate{0};
147 unsigned int d_seconds{0};
148 DNSAction::Action d_action{DNSAction::Action::None};
149 bool d_enabled{false};
150 };
151
152 typedef std::unordered_map<ComboAddress, Counts, ComboAddress::addressOnlyHash, ComboAddress::addressOnlyEqual> counts_t;
153
154 public:
155 DynBlockRulesGroup()
156 {
157 }
158
159 void setQueryRate(unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
160 {
161 d_queryRateRule = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
162 }
163
164 /* rate is in bytes per second */
165 void setResponseByteRate(unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
166 {
167 d_respRateRule = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
168 }
169
170 void setRCodeRate(uint8_t rcode, unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
171 {
172 auto& entry = d_rcodeRules[rcode];
173 entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
174 }
175
176 void setQTypeRate(uint16_t qtype, unsigned int rate, unsigned int warningRate, unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action)
177 {
178 auto& entry = d_qtypeRules[qtype];
179 entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action);
180 }
181
182 typedef std::function<bool(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)> smtVisitor_t;
183
184 void setSuffixMatchRule(unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action, smtVisitor_t visitor)
185 {
186 d_suffixMatchRule = DynBlockRule(reason, blockDuration, 0, 0, seconds, action);
187 d_smtVisitor = visitor;
188 }
189
190 void setSuffixMatchRuleFFI(unsigned int seconds, std::string reason, unsigned int blockDuration, DNSAction::Action action, dnsdist_ffi_stat_node_visitor_t visitor)
191 {
192 d_suffixMatchRule = DynBlockRule(reason, blockDuration, 0, 0, seconds, action);
193 d_smtVisitorFFI = visitor;
194 }
195
196 void apply()
197 {
198 struct timespec now;
199 gettime(&now);
200
201 apply(now);
202 }
203
204 void apply(const struct timespec& now)
205 {
206 counts_t counts;
207 StatNode statNodeRoot;
208
209 size_t entriesCount = 0;
210 if (hasQueryRules()) {
211 entriesCount += g_rings.getNumberOfQueryEntries();
212 }
213 if (hasResponseRules()) {
214 entriesCount += g_rings.getNumberOfResponseEntries();
215 }
216 counts.reserve(entriesCount);
217
218 processQueryRules(counts, now);
219 processResponseRules(counts, statNodeRoot, now);
220
221 if (counts.empty() && statNodeRoot.empty()) {
222 return;
223 }
224
225 boost::optional<NetmaskTree<DynBlock> > blocks;
226 bool updated = false;
227
228 for (const auto& entry : counts) {
229 const auto& requestor = entry.first;
230 const auto& counters = entry.second;
231
232 if (d_queryRateRule.warningRateExceeded(counters.queries, now)) {
233 handleWarning(blocks, now, requestor, d_queryRateRule, updated);
234 }
235
236 if (d_queryRateRule.rateExceeded(counters.queries, now)) {
237 addBlock(blocks, now, requestor, d_queryRateRule, updated);
238 continue;
239 }
240
241 if (d_respRateRule.warningRateExceeded(counters.respBytes, now)) {
242 handleWarning(blocks, now, requestor, d_respRateRule, updated);
243 }
244
245 if (d_respRateRule.rateExceeded(counters.respBytes, now)) {
246 addBlock(blocks, now, requestor, d_respRateRule, updated);
247 continue;
248 }
249
250 for (const auto& pair : d_qtypeRules) {
251 const auto qtype = pair.first;
252
253 const auto& typeIt = counters.d_qtypeCounts.find(qtype);
254 if (typeIt != counters.d_qtypeCounts.cend()) {
255
256 if (pair.second.warningRateExceeded(typeIt->second, now)) {
257 handleWarning(blocks, now, requestor, pair.second, updated);
258 }
259
260 if (pair.second.rateExceeded(typeIt->second, now)) {
261 addBlock(blocks, now, requestor, pair.second, updated);
262 break;
263 }
264 }
265 }
266
267 for (const auto& pair : d_rcodeRules) {
268 const auto rcode = pair.first;
269
270 const auto& rcodeIt = counters.d_rcodeCounts.find(rcode);
271 if (rcodeIt != counters.d_rcodeCounts.cend()) {
272 if (pair.second.warningRateExceeded(rcodeIt->second, now)) {
273 handleWarning(blocks, now, requestor, pair.second, updated);
274 }
275
276 if (pair.second.rateExceeded(rcodeIt->second, now)) {
277 addBlock(blocks, now, requestor, pair.second, updated);
278 break;
279 }
280 }
281 }
282 }
283
284 if (updated && blocks) {
285 g_dynblockNMG.setState(*blocks);
286 }
287
288 if (!statNodeRoot.empty()) {
289 StatNode::Stat node;
290 std::unordered_set<DNSName> namesToBlock;
291 statNodeRoot.visit([this,&namesToBlock](const StatNode* node_, const StatNode::Stat& self, const StatNode::Stat& children) {
292 bool block = false;
293
294 if (d_smtVisitorFFI) {
295 dnsdist_ffi_stat_node_t tmp(*node_, self, children);
296 block = d_smtVisitorFFI(&tmp);
297 }
298 else {
299 block = d_smtVisitor(*node_, self, children);
300 }
301
302 if (block) {
303 namesToBlock.insert(DNSName(node_->fullname));
304 }
305 },
306 node);
307
308 if (!namesToBlock.empty()) {
309 updated = false;
310 SuffixMatchTree<DynBlock> smtBlocks = g_dynblockSMT.getCopy();
311 for (const auto& name : namesToBlock) {
312 addOrRefreshBlockSMT(smtBlocks, now, name, d_suffixMatchRule, updated);
313 }
314 if (updated) {
315 g_dynblockSMT.setState(smtBlocks);
316 }
317 }
318 }
319 }
320
321 void excludeRange(const Netmask& range)
322 {
323 d_excludedSubnets.addMask(range);
324 }
325
326 void includeRange(const Netmask& range)
327 {
328 d_excludedSubnets.addMask(range, false);
329 }
330
331 void excludeDomain(const DNSName& domain)
332 {
333 d_excludedDomains.add(domain);
334 }
335
336 std::string toString() const
337 {
338 std::stringstream result;
339
340 result << "Query rate rule: " << d_queryRateRule.toString() << std::endl;
341 result << "Response rate rule: " << d_respRateRule.toString() << std::endl;
342 result << "SuffixMatch rule: " << d_suffixMatchRule.toString() << std::endl;
343 result << "RCode rules: " << std::endl;
344 for (const auto& rule : d_rcodeRules) {
345 result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl;
346 }
347 result << "QType rules: " << std::endl;
348 for (const auto& rule : d_qtypeRules) {
349 result << "- " << QType(rule.first).getName() << ": " << rule.second.toString() << std::endl;
350 }
351 result << "Excluded Subnets: " << d_excludedSubnets.toString() << std::endl;
352 result << "Excluded Domains: " << d_excludedDomains.toString() << std::endl;
353
354 return result.str();
355 }
356
357 void setQuiet(bool quiet)
358 {
359 d_beQuiet = quiet;
360 }
361
362 private:
363 bool checkIfQueryTypeMatches(const Rings::Query& query)
364 {
365 auto rule = d_qtypeRules.find(query.qtype);
366 if (rule == d_qtypeRules.end()) {
367 return false;
368 }
369
370 return rule->second.matches(query.when);
371 }
372
373 bool checkIfResponseCodeMatches(const Rings::Response& response)
374 {
375 auto rule = d_rcodeRules.find(response.dh.rcode);
376 if (rule == d_rcodeRules.end()) {
377 return false;
378 }
379
380 return rule->second.matches(response.when);
381 }
382
383 void addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated, bool warning)
384 {
385 if (d_excludedSubnets.match(requestor)) {
386 /* do not add a block for excluded subnets */
387 return;
388 }
389
390 if (!blocks) {
391 blocks = g_dynblockNMG.getCopy();
392 }
393 struct timespec until = now;
394 until.tv_sec += rule.d_blockDuration;
395 unsigned int count = 0;
396 const auto& got = blocks->lookup(Netmask(requestor));
397 bool expired = false;
398 bool wasWarning = false;
399
400 if (got) {
401 if (warning && !got->second.warning) {
402 /* we have an existing entry which is not a warning,
403 don't override it */
404 return;
405 }
406 else if (!warning && got->second.warning) {
407 wasWarning = true;
408 }
409 else {
410 if (until < got->second.until) {
411 // had a longer policy
412 return;
413 }
414 }
415
416 if (now < got->second.until) {
417 // only inherit count on fresh query we are extending
418 count = got->second.blocks;
419 }
420 else {
421 expired = true;
422 }
423 }
424
425 DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action};
426 db.blocks = count;
427 db.warning = warning;
428 if (!d_beQuiet && (!got || expired || wasWarning)) {
429 warnlog("Inserting %sdynamic block for %s for %d seconds: %s", warning ? "(warning) " :"", requestor.toString(), rule.d_blockDuration, rule.d_blockReason);
430 }
431 blocks->insert(Netmask(requestor)).second = db;
432 updated = true;
433 }
434
435 void addOrRefreshBlockSMT(SuffixMatchTree<DynBlock>& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated)
436 {
437 if (d_excludedDomains.check(name)) {
438 /* do not add a block for excluded domains */
439 return;
440 }
441
442 struct timespec until = now;
443 until.tv_sec += rule.d_blockDuration;
444 unsigned int count = 0;
445 const auto& got = blocks.lookup(name);
446 bool expired = false;
447
448 if (got) {
449 if (until < got->until) {
450 // had a longer policy
451 return;
452 }
453
454 if (now < got->until) {
455 // only inherit count on fresh query we are extending
456 count = got->blocks;
457 }
458 else {
459 expired = true;
460 }
461 }
462
463 DynBlock db{rule.d_blockReason, until, name, rule.d_action};
464 db.blocks = count;
465
466 if (!d_beQuiet && (!got || expired)) {
467 warnlog("Inserting dynamic block for %s for %d seconds: %s", name, rule.d_blockDuration, rule.d_blockReason);
468 }
469 blocks.add(name, db);
470 updated = true;
471 }
472
473 void addBlock(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
474 {
475 addOrRefreshBlock(blocks, now, requestor, rule, updated, false);
476 }
477
478 void handleWarning(boost::optional<NetmaskTree<DynBlock> >& blocks, const struct timespec& now, const ComboAddress& requestor, const DynBlockRule& rule, bool& updated)
479 {
480 addOrRefreshBlock(blocks, now, requestor, rule, updated, true);
481 }
482
483 bool hasQueryRules() const
484 {
485 return d_queryRateRule.isEnabled() || !d_qtypeRules.empty();
486 }
487
488 bool hasResponseRules() const
489 {
490 return d_respRateRule.isEnabled() || !d_rcodeRules.empty();
491 }
492
493 bool hasSuffixMatchRules() const
494 {
495 return d_suffixMatchRule.isEnabled();
496 }
497
498 bool hasRules() const
499 {
500 return hasQueryRules() || hasResponseRules();
501 }
502
503 void processQueryRules(counts_t& counts, const struct timespec& now)
504 {
505 if (!hasQueryRules()) {
506 return;
507 }
508
509 d_queryRateRule.d_cutOff = d_queryRateRule.d_minTime = now;
510 d_queryRateRule.d_cutOff.tv_sec -= d_queryRateRule.d_seconds;
511
512 for (auto& rule : d_qtypeRules) {
513 rule.second.d_cutOff = rule.second.d_minTime = now;
514 rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
515 }
516
517 for (const auto& shard : g_rings.d_shards) {
518 std::lock_guard<std::mutex> rl(shard->queryLock);
519 for(const auto& c : shard->queryRing) {
520 if (now < c.when) {
521 continue;
522 }
523
524 bool qRateMatches = d_queryRateRule.matches(c.when);
525 bool typeRuleMatches = checkIfQueryTypeMatches(c);
526
527 if (qRateMatches || typeRuleMatches) {
528 auto& entry = counts[c.requestor];
529 if (qRateMatches) {
530 entry.queries++;
531 }
532 if (typeRuleMatches) {
533 entry.d_qtypeCounts[c.qtype]++;
534 }
535 }
536 }
537 }
538 }
539
540 void processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now)
541 {
542 if (!hasResponseRules() && !hasSuffixMatchRules()) {
543 return;
544 }
545
546 d_respRateRule.d_cutOff = d_respRateRule.d_minTime = now;
547 d_respRateRule.d_cutOff.tv_sec -= d_respRateRule.d_seconds;
548
549 d_suffixMatchRule.d_cutOff = d_suffixMatchRule.d_minTime = now;
550 d_suffixMatchRule.d_cutOff.tv_sec -= d_suffixMatchRule.d_seconds;
551
552 for (auto& rule : d_rcodeRules) {
553 rule.second.d_cutOff = rule.second.d_minTime = now;
554 rule.second.d_cutOff.tv_sec -= rule.second.d_seconds;
555 }
556
557 for (const auto& shard : g_rings.d_shards) {
558 std::lock_guard<std::mutex> rl(shard->respLock);
559 for(const auto& c : shard->respRing) {
560 if (now < c.when) {
561 continue;
562 }
563
564 bool respRateMatches = d_respRateRule.matches(c.when);
565 bool suffixMatchRuleMatches = d_suffixMatchRule.matches(c.when);
566 bool rcodeRuleMatches = checkIfResponseCodeMatches(c);
567
568 if (respRateMatches || rcodeRuleMatches) {
569 auto& entry = counts[c.requestor];
570 if (respRateMatches) {
571 entry.respBytes += c.size;
572 }
573 if (rcodeRuleMatches) {
574 entry.d_rcodeCounts[c.dh.rcode]++;
575 }
576 }
577
578 if (suffixMatchRuleMatches) {
579 root.submit(c.name, c.dh.rcode, boost::none);
580 }
581 }
582 }
583 }
584
585 std::map<uint8_t, DynBlockRule> d_rcodeRules;
586 std::map<uint16_t, DynBlockRule> d_qtypeRules;
587 DynBlockRule d_queryRateRule;
588 DynBlockRule d_respRateRule;
589 DynBlockRule d_suffixMatchRule;
590 NetmaskGroup d_excludedSubnets;
591 SuffixMatchNode d_excludedDomains;
592 smtVisitor_t d_smtVisitor;
593 dnsdist_ffi_stat_node_visitor_t d_smtVisitorFFI;
594 bool d_beQuiet{false};
595 };