Coverage Report

Created: 2026-10-16 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pdns/pdns/dnsdistdist/dnsdist-dynblocks.hh
Line
Count
Source
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
#ifndef DISABLE_DYNBLOCKS
25
#include <unordered_set>
26
27
#include "dolog.hh"
28
#include "dnsdist-rings.hh"
29
#include "gettime.hh"
30
#include "statnode.hh"
31
32
extern "C"
33
{
34
#include "dnsdist-lua-inspection-ffi.h"
35
}
36
37
#include "ext/luawrapper/include/LuaContext.hpp"
38
39
// dnsdist_ffi_stat_node_t is a lightuserdata
40
template <>
41
struct LuaContext::Pusher<dnsdist_ffi_stat_node_t*>
42
{
43
  static const int minSize = 1;
44
  static const int maxSize = 1;
45
46
  static PushedObject push(lua_State* state, dnsdist_ffi_stat_node_t* ptr) noexcept
47
0
  {
48
0
    lua_pushlightuserdata(state, ptr);
49
0
    return PushedObject{state, 1};
50
0
  }
51
};
52
53
using dnsdist_ffi_stat_node_visitor_t = std::function<bool(dnsdist_ffi_stat_node_t*)>;
54
55
struct SMTBlockParameters
56
{
57
  std::optional<std::string> d_reason;
58
  std::optional<DNSAction::Action> d_action;
59
};
60
61
struct dnsdist_ffi_stat_node_t
62
{
63
  dnsdist_ffi_stat_node_t(const StatNode& node_, const StatNode::Stat& self_, const StatNode::Stat& children_, SMTBlockParameters& blockParameters) :
64
0
    node(node_), self(self_), children(children_), d_blockParameters(blockParameters)
65
0
  {
66
0
  }
67
68
  mutable std::string fullname;
69
  const StatNode& node;
70
  const StatNode::Stat& self;
71
  const StatNode::Stat& children;
72
  SMTBlockParameters& d_blockParameters;
73
};
74
75
struct DynBlock
76
{
77
  DynBlock()
78
12
  {
79
12
    until.tv_sec = 0;
80
12
    until.tv_nsec = 0;
81
12
  }
82
83
  DynBlock(const std::string& reason_, const struct timespec& until_, const DNSName& domain_, DNSAction::Action action_) :
84
0
    reason(reason_), domain(domain_), until(until_), action(action_)
85
0
  {
86
0
  }
87
88
  DynBlock(const DynBlock& rhs) :
89
0
    reason(rhs.reason), domain(rhs.domain), until(rhs.until), tagSettings(rhs.tagSettings), action(rhs.action), warning(rhs.warning), bpf(rhs.bpf)
90
0
  {
91
0
    blocks.store(rhs.blocks);
92
0
  }
93
94
  DynBlock(DynBlock&& rhs) :
95
0
    reason(std::move(rhs.reason)), domain(std::move(rhs.domain)), until(rhs.until), tagSettings(std::move(rhs.tagSettings)), action(rhs.action), warning(rhs.warning), bpf(rhs.bpf)
96
0
  {
97
0
    blocks.store(rhs.blocks);
98
0
  }
99
100
  DynBlock& operator=(const DynBlock& rhs)
101
0
  {
102
0
    reason = rhs.reason;
103
0
    until = rhs.until;
104
0
    domain = rhs.domain;
105
0
    action = rhs.action;
106
0
    blocks.store(rhs.blocks);
107
0
    warning = rhs.warning;
108
0
    bpf = rhs.bpf;
109
0
    tagSettings = rhs.tagSettings;
110
0
    return *this;
111
0
  }
112
113
  DynBlock& operator=(DynBlock&& rhs)
114
0
  {
115
0
    reason = std::move(rhs.reason);
116
0
    until = rhs.until;
117
0
    domain = std::move(rhs.domain);
118
0
    action = rhs.action;
119
0
    blocks.store(rhs.blocks);
120
0
    warning = rhs.warning;
121
0
    bpf = rhs.bpf;
122
0
    tagSettings = std::move(rhs.tagSettings);
123
0
    return *this;
124
0
  }
125
126
  struct TagSettings
127
  {
128
    std::string d_name;
129
    std::string d_value;
130
  };
131
132
  string reason;
133
  DNSName domain;
134
  timespec until{};
135
  std::shared_ptr<TagSettings> tagSettings{nullptr};
136
  mutable std::atomic<uint32_t> blocks{0};
137
  DNSAction::Action action{DNSAction::Action::None};
138
  bool warning{false};
139
  bool bpf{false};
140
};
141
142
using dnsdist_ffi_dynamic_block_inserted_hook = std::function<void(uint8_t type, const char* key, const char* reason, uint8_t action, uint64_t duration, bool warning)>;
143
using ClientAddressDynamicRules = NetmaskTree<DynBlock, AddressAndPortRange>;
144
using SuffixDynamicRules = SuffixMatchTree<DynBlock>;
145
146
class DynBlockRulesGroup
147
{
148
public:
149
  struct DynBlockRule
150
  {
151
0
    DynBlockRule() = default;
152
    DynBlockRule(const std::string& blockReason, uint32_t blockDuration, uint32_t rate, uint32_t warningRate, uint32_t seconds, DNSAction::Action action) :
153
0
      d_blockReason(blockReason), d_blockDuration(blockDuration), d_rate(rate), d_warningRate(warningRate), d_seconds(seconds), d_action(action), d_enabled(true)
154
0
    {
155
0
    }
156
157
    bool matches(const struct timespec& when);
158
    bool rateExceeded(uint32_t count, const struct timespec& now) const;
159
    bool warningRateExceeded(uint32_t count, const struct timespec& now) const;
160
161
    bool isEnabled() const
162
0
    {
163
0
      return d_enabled;
164
0
    }
165
166
    std::string toString() const;
167
168
    std::string d_blockReason;
169
    std::shared_ptr<DynBlock::TagSettings> d_tagSettings;
170
    struct timespec d_cutOff;
171
    struct timespec d_minTime;
172
    uint32_t d_blockDuration{0};
173
    uint32_t d_rate{0};
174
    uint32_t d_warningRate{0};
175
    uint32_t d_seconds{0};
176
    DNSAction::Action d_action{DNSAction::Action::None};
177
    bool d_enabled{false};
178
  };
179
180
  struct DynBlockRatioRule : DynBlockRule
181
  {
182
0
    DynBlockRatioRule() = default;
183
    DynBlockRatioRule(const std::string& blockReason, uint32_t blockDuration, double ratio, double warningRatio, uint32_t seconds, DNSAction::Action action, size_t minimumNumberOfResponses) :
184
0
      DynBlockRule(blockReason, blockDuration, 0, 0, seconds, action), d_minimumNumberOfResponses(minimumNumberOfResponses), d_ratio(ratio), d_warningRatio(warningRatio)
185
0
    {
186
0
    }
187
188
    bool ratioExceeded(uint32_t total, uint32_t count) const;
189
    bool warningRatioExceeded(uint32_t total, uint32_t count) const;
190
    std::string toString() const;
191
192
    size_t d_minimumNumberOfResponses{0};
193
    double d_ratio{0.0};
194
    double d_warningRatio{0.0};
195
  };
196
197
  struct DynBlockAllowedRCodesRatioRule : DynBlockRatioRule
198
  {
199
0
    DynBlockAllowedRCodesRatioRule() = default;
200
    DynBlockAllowedRCodesRatioRule(const std::unordered_set<uint8_t>& rcodes, const std::string& blockReason, uint32_t blockDuration, double ratio, double warningRatio, uint32_t seconds, DNSAction::Action action, size_t minimumNumberOfResponses) :
201
0
      DynBlockRatioRule(blockReason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses), d_allowedRCodes(rcodes)
202
0
    {
203
0
    }
204
    bool isRCodeAllowed(uint8_t rcode) const;
205
    std::string toString() const;
206
207
    std::unordered_set<uint8_t> d_allowedRCodes{};
208
  };
209
210
  struct DynBlockCacheMissRatioRule : public DynBlockRatioRule
211
  {
212
0
    DynBlockCacheMissRatioRule() = default;
213
    DynBlockCacheMissRatioRule(const std::string& blockReason, uint32_t blockDuration, double ratio, double warningRatio, uint32_t seconds, DNSAction::Action action, size_t minimumNumberOfResponses, double minimumGlobalCacheHitRatio) :
214
0
      DynBlockRatioRule(blockReason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses), d_minimumGlobalCacheHitRatio(minimumGlobalCacheHitRatio)
215
0
    {
216
0
    }
217
218
    bool checkGlobalCacheHitRatio() const;
219
    bool ratioExceeded(uint32_t total, uint32_t count) const;
220
    bool warningRatioExceeded(uint32_t total, uint32_t count) const;
221
    std::string toString() const;
222
223
    double d_minimumGlobalCacheHitRatio{0.0};
224
  };
225
226
private:
227
  struct Counts
228
  {
229
    std::map<uint8_t, uint64_t> d_rcodeCounts;
230
    std::map<uint16_t, uint64_t> d_qtypeCounts;
231
    uint64_t queries{0};
232
    uint64_t responses{0};
233
    uint64_t respBytes{0};
234
    uint64_t cacheMisses{0};
235
    uint64_t notAllowedRCodes{0};
236
  };
237
  using counts_t = std::unordered_map<AddressAndPortRange, Counts, AddressAndPortRange::hash>;
238
239
public:
240
  DynBlockRulesGroup()
241
0
  {
242
0
  }
243
244
  void setQueryRate(DynBlockRule&& rule)
245
0
  {
246
0
    d_queryRateRule = std::move(rule);
247
0
  }
248
249
  /* rate is in bytes per second */
250
  void setResponseByteRate(DynBlockRule&& rule)
251
0
  {
252
0
    d_respRateRule = std::move(rule);
253
0
  }
254
255
  void setRCodeRate(uint8_t rcode, DynBlockRule&& rule)
256
0
  {
257
0
    d_rcodeRules[rcode] = std::move(rule);
258
0
  }
259
260
  void setRCodeRatio(uint8_t rcode, DynBlockRatioRule&& rule)
261
0
  {
262
0
    d_rcodeRatioRules[rcode] = std::move(rule);
263
0
  }
264
265
  void setAllowedRCodesRatio(DynBlockAllowedRCodesRatioRule&& rule)
266
0
  {
267
0
    d_allowedRCodesRatioRule = std::move(rule);
268
0
  }
269
270
  void setQTypeRate(uint16_t qtype, DynBlockRule&& rule)
271
0
  {
272
0
    d_qtypeRules[qtype] = std::move(rule);
273
0
  }
274
275
  void setCacheMissRatio(DynBlockCacheMissRatioRule&& rule)
276
0
  {
277
0
    d_respCacheMissRatioRule = std::move(rule);
278
0
  }
279
280
  using smtVisitor_t = std::function<std::tuple<bool, std::optional<std::string>, std::optional<int>>(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)>;
281
282
  void setSuffixMatchRule(DynBlockRule&& rule, smtVisitor_t visitor)
283
0
  {
284
0
    d_suffixMatchRule = std::move(rule);
285
0
    d_smtVisitor = std::move(visitor);
286
0
  }
287
288
  void setSuffixMatchRuleFFI(DynBlockRule&& rule, dnsdist_ffi_stat_node_visitor_t visitor)
289
0
  {
290
0
    d_suffixMatchRule = std::move(rule);
291
0
    d_smtVisitorFFI = std::move(visitor);
292
0
  }
293
294
  void setNewBlockHook(const dnsdist_ffi_dynamic_block_inserted_hook& callback)
295
0
  {
296
0
    d_newBlockHook = callback;
297
0
  }
298
299
  void setMasks(uint8_t v4, uint8_t v6, uint8_t port)
300
0
  {
301
0
    d_v4Mask = v4;
302
0
    d_v6Mask = v6;
303
0
    d_portMask = port;
304
0
  }
305
306
  void apply()
307
0
  {
308
0
    timespec now{};
309
0
    gettime(&now);
310
311
0
    apply(now);
312
0
  }
313
314
  void apply(const timespec& now);
315
316
  void excludeRange(const Netmask& range)
317
0
  {
318
0
    d_excludedSubnets.addMask(range);
319
0
  }
320
321
  void excludeRange(const NetmaskGroup& group)
322
0
  {
323
0
    d_excludedSubnets.addMasks(group, true);
324
0
  }
325
326
  void includeRange(const Netmask& range)
327
0
  {
328
0
    d_excludedSubnets.addMask(range, false);
329
0
  }
330
331
  void includeRange(const NetmaskGroup& group)
332
0
  {
333
0
    d_excludedSubnets.addMasks(group, false);
334
0
  }
335
336
  void removeRange(const Netmask& range)
337
0
  {
338
0
    d_excludedSubnets.deleteMask(range);
339
0
  }
340
341
  void removeRange(const NetmaskGroup& group)
342
0
  {
343
0
    d_excludedSubnets.deleteMasks(group);
344
0
  }
345
346
  void excludeDomain(const DNSName& domain)
347
0
  {
348
0
    d_excludedDomains.add(domain);
349
0
  }
350
351
  std::string toString() const
352
0
  {
353
0
    std::stringstream result;
354
355
0
    result << "Query rate rule: " << d_queryRateRule.toString() << std::endl;
356
0
    result << "Response rate rule: " << d_respRateRule.toString() << std::endl;
357
0
    result << "SuffixMatch rule: " << d_suffixMatchRule.toString() << std::endl;
358
0
    result << "Response cache-miss ratio rule: " << d_respCacheMissRatioRule.toString() << std::endl;
359
0
    result << "RCode rules: " << std::endl;
360
0
    for (const auto& rule : d_rcodeRules) {
361
0
      result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl;
362
0
    }
363
0
    for (const auto& rule : d_rcodeRatioRules) {
364
0
      result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl;
365
0
    }
366
0
    result << "Allowed rcodes ratio rule: " << d_allowedRCodesRatioRule.toString() << std::endl;
367
0
    result << "QType rules: " << std::endl;
368
0
    for (const auto& rule : d_qtypeRules) {
369
0
      result << "- " << QType(rule.first).toString() << ": " << rule.second.toString() << std::endl;
370
0
    }
371
0
    result << "Excluded Subnets: " << d_excludedSubnets.toString() << std::endl;
372
0
    result << "Excluded Domains: " << d_excludedDomains.toString() << std::endl;
373
374
0
    return result.str();
375
0
  }
376
377
  void setQuiet(bool quiet)
378
0
  {
379
0
    d_beQuiet = quiet;
380
0
  }
381
382
private:
383
  void applySMT(const struct timespec& now, StatNode& statNodeRoot);
384
  bool checkIfQueryTypeMatches(const Rings::Query& query);
385
  bool checkIfResponseCodeMatches(const Rings::Response& response);
386
  void addOrRefreshBlock(std::optional<ClientAddressDynamicRules>& blocks, const struct timespec& now, const AddressAndPortRange& requestor, const DynBlockRule& rule, bool& updated, bool warning);
387
  void addOrRefreshBlockSMT(SuffixDynamicRules& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated);
388
389
  void addBlock(std::optional<ClientAddressDynamicRules>& blocks, const struct timespec& now, const AddressAndPortRange& requestor, const DynBlockRule& rule, bool& updated)
390
0
  {
391
0
    addOrRefreshBlock(blocks, now, requestor, rule, updated, false);
392
0
  }
393
394
  void handleWarning(std::optional<ClientAddressDynamicRules>& blocks, const struct timespec& now, const AddressAndPortRange& requestor, const DynBlockRule& rule, bool& updated)
395
0
  {
396
0
    addOrRefreshBlock(blocks, now, requestor, rule, updated, true);
397
0
  }
398
399
  bool hasQueryRules() const
400
0
  {
401
0
    return d_queryRateRule.isEnabled() || !d_qtypeRules.empty();
402
0
  }
403
404
  bool hasResponseRules() const
405
0
  {
406
0
    return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty() || d_allowedRCodesRatioRule.isEnabled() || d_respCacheMissRatioRule.isEnabled();
407
0
  }
408
409
  bool hasSuffixMatchRules() const
410
0
  {
411
0
    return d_suffixMatchRule.isEnabled();
412
0
  }
413
414
  bool hasRules() const
415
0
  {
416
0
    return hasQueryRules() || hasResponseRules();
417
0
  }
418
419
  void processQueryRules(counts_t& counts, const struct timespec& now);
420
  void processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now);
421
422
  std::map<uint8_t, DynBlockRule> d_rcodeRules;
423
  std::map<uint8_t, DynBlockRatioRule> d_rcodeRatioRules;
424
  std::map<uint16_t, DynBlockRule> d_qtypeRules;
425
  DynBlockRule d_queryRateRule;
426
  DynBlockRule d_respRateRule;
427
  DynBlockRule d_suffixMatchRule;
428
  DynBlockCacheMissRatioRule d_respCacheMissRatioRule;
429
  DynBlockAllowedRCodesRatioRule d_allowedRCodesRatioRule;
430
  NetmaskGroup d_excludedSubnets;
431
  SuffixMatchNode d_excludedDomains;
432
  smtVisitor_t d_smtVisitor;
433
  dnsdist_ffi_stat_node_visitor_t d_smtVisitorFFI;
434
  dnsdist_ffi_dynamic_block_inserted_hook d_newBlockHook;
435
  uint8_t d_v6Mask{128};
436
  uint8_t d_v4Mask{32};
437
  uint8_t d_portMask{0};
438
  bool d_beQuiet{false};
439
};
440
441
class DynBlockMaintenance
442
{
443
public:
444
  static void run();
445
446
  /* return the (cached) number of hits per second for the top offenders, averaged over 60s */
447
  static std::map<std::string, std::list<std::pair<AddressAndPortRange, uint32_t>>> getHitsForTopNetmasks();
448
  static std::map<std::string, std::list<std::pair<DNSName, uint32_t>>> getHitsForTopSuffixes();
449
450
  /* get the top offenders based on the current value of the counters */
451
  static std::map<std::string, std::list<std::pair<AddressAndPortRange, uint32_t>>> getTopNetmasks(size_t topN);
452
  static std::map<std::string, std::list<std::pair<DNSName, uint32_t>>> getTopSuffixes(size_t topN);
453
  static void purgeExpired(const struct timespec& now);
454
455
private:
456
  static void collectMetrics();
457
  static void generateMetrics();
458
459
  struct MetricsSnapshot
460
  {
461
    std::map<std::string, std::list<std::pair<AddressAndPortRange, uint32_t>>> nmgData;
462
    std::map<std::string, std::list<std::pair<DNSName, uint32_t>>> smtData;
463
  };
464
465
  struct Tops
466
  {
467
    std::map<std::string, std::list<std::pair<AddressAndPortRange, uint32_t>>> topNMGsByReason;
468
    std::map<std::string, std::list<std::pair<DNSName, uint32_t>>> topSMTsByReason;
469
  };
470
471
  static LockGuarded<Tops> s_tops;
472
  /* s_metricsData should only be accessed by the dynamic blocks maintenance thread so it does not need a lock */
473
  // need N+1 datapoints to be able to do the diff after a collection point has been reached
474
  static std::list<MetricsSnapshot> s_metricsData;
475
  static constexpr size_t s_topN{20};
476
};
477
478
namespace dnsdist::DynamicBlocks
479
{
480
bool addOrRefreshBlock(ClientAddressDynamicRules& blocks, const timespec& now, const AddressAndPortRange& requestor, DynBlock&& dblock, bool beQuiet);
481
bool addOrRefreshBlockSMT(SuffixDynamicRules& blocks, const timespec& now, DynBlock&& dblock, bool beQuiet);
482
483
const ClientAddressDynamicRules& getClientAddressDynamicRules();
484
const SuffixDynamicRules& getSuffixDynamicRules();
485
ClientAddressDynamicRules getClientAddressDynamicRulesCopy();
486
SuffixDynamicRules getSuffixDynamicRulesCopy();
487
void setClientAddressDynamicRules(ClientAddressDynamicRules&& rules);
488
void setSuffixDynamicRules(SuffixDynamicRules&& rules);
489
void clearClientAddressDynamicRules();
490
void clearSuffixDynamicRules();
491
492
void registerGroup(std::shared_ptr<DynBlockRulesGroup>& group);
493
void runRegisteredGroups(LuaContext& luaCtx);
494
}
495
#endif /* DISABLE_DYNBLOCKS */