• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PowerDNS / pdns / 18743945403

23 Oct 2025 09:29AM UTC coverage: 65.845% (+0.02%) from 65.829%
18743945403

Pull #16356

github

web-flow
Merge 8a2027ef1 into efa3637e8
Pull Request #16356: auth 5.0: backport "pdnsutil: fix b2b-migrate to from sql to non-sql"

42073 of 92452 branches covered (45.51%)

Branch coverage included in aggregate %.

128008 of 165855 relevant lines covered (77.18%)

6379935.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.81
/pdns/recursordist/rpzloader.cc
1
/*
2
 * This file is part of PowerDNS or dnsdist.
3
 * Copyright -- PowerDNS.COM B.V. and its contributors
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of version 2 of the GNU General Public License as
7
 * published by the Free Software Foundation.
8
 *
9
 * In addition, for the avoidance of any doubt, permission is granted to
10
 * link this program with OpenSSL and to (re)distribute the binaries
11
 * produced as the result of such linking.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
 */
22
#include <condition_variable>
23
#include "arguments.hh"
24
#include "dnsparser.hh"
25
#include "dnsrecords.hh"
26
#include "ixfr.hh"
27
#include "axfr-retriever.hh"
28
#include "lock.hh"
29
#include "logging.hh"
30
#include "rec-lua-conf.hh"
31
#include "rpzloader.hh"
32
#include "zoneparser-tng.hh"
33
#include "threadname.hh"
34
#include "query-local-address.hh"
35
#include "rec-system-resolve.hh"
36

37
Netmask makeNetmaskFromRPZ(const DNSName& name)
38
{
52✔
39
  auto parts = name.getRawLabels();
52✔
40
  /*
41
   * why 2?, the minimally valid IPv6 address that can be encoded in an RPZ is
42
   * $NETMASK.zz (::/$NETMASK)
43
   * Terrible right?
44
   */
45
  if (parts.size() < 2 || parts.size() > 9) {
52!
46
    throw PDNSException("Invalid IP address in RPZ: " + name.toLogString());
×
47
  }
×
48

49
  bool isV6 = (stoi(parts[0]) > 32);
52✔
50
  bool hadZZ = false;
52✔
51

52
  for (auto& part : parts) {
290✔
53
    // Check if we have an IPv4 octet
54
    for (auto labelLetter : part) {
537✔
55
      if (isdigit(labelLetter) == 0) {
537✔
56
        isV6 = true;
48✔
57
      }
48✔
58
    }
537✔
59
    if (pdns_iequals(part, "zz")) {
290✔
60
      if (hadZZ) {
10!
61
        throw PDNSException("more than one 'zz' label found in RPZ name" + name.toLogString());
×
62
      }
×
63
      part = "";
10✔
64
      isV6 = true;
10✔
65
      hadZZ = true;
10✔
66
    }
10✔
67
  }
290✔
68

69
  if (isV6 && parts.size() < 9 && !hadZZ) {
52!
70
    throw PDNSException("No 'zz' label found in an IPv6 RPZ name shorter than 9 elements: " + name.toLogString());
×
71
  }
×
72

73
  if (parts.size() == 5 && !isV6) {
52✔
74
    return parts[4] + "." + parts[3] + "." + parts[2] + "." + parts[1] + "/" + parts[0];
36✔
75
  }
36✔
76
  string v6Address;
16✔
77

78
  if (parts[parts.size() - 1].empty()) {
16✔
79
    v6Address += ":";
2✔
80
  }
2✔
81
  for (uint8_t i = parts.size() - 1; i > 0; i--) {
110✔
82
    v6Address += parts[i];
94✔
83
    if (i > 1 || (i == 1 && parts[i].empty())) {
94!
84
      v6Address += ":";
84✔
85
    }
84✔
86
  }
94✔
87
  v6Address += "/" + parts[0];
16✔
88

89
  return v6Address;
16✔
90
}
52✔
91

92
static void RPZRecordToPolicy(const DNSRecord& dnsRecord, const std::shared_ptr<DNSFilterEngine::Zone>& zone, bool addOrRemove, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, Logr::log_t log)
93
{
120✔
94
  static const DNSName drop("rpz-drop.");
120✔
95
  static const DNSName truncate("rpz-tcp-only.");
120✔
96
  static const DNSName noaction("rpz-passthru.");
120✔
97
  static const DNSName rpzClientIP("rpz-client-ip");
120✔
98
  static const DNSName rpzIP("rpz-ip");
120✔
99
  static const DNSName rpzNSDname("rpz-nsdname");
120✔
100
  static const DNSName rpzNSIP("rpz-nsip.");
120✔
101
  static const std::string rpzPrefix("rpz-");
120✔
102

103
  DNSFilterEngine::Policy pol;
120✔
104
  bool defpolApplied = false;
120✔
105

106
  if (dnsRecord.d_class != QClass::IN) {
120!
107
    return;
×
108
  }
×
109

110
  if (dnsRecord.d_type == QType::CNAME) {
120✔
111
    auto crc = getRR<CNAMERecordContent>(dnsRecord);
61✔
112
    if (!crc) {
61!
113
      return;
×
114
    }
×
115
    auto crcTarget = crc->getTarget();
61✔
116
    if (defpol) {
61✔
117
      pol = *defpol;
4✔
118
      defpolApplied = true;
4✔
119
    }
4✔
120
    else if (crcTarget.isRoot()) {
57✔
121
      // cerr<<"Wants NXDOMAIN for "<<dr.d_name<<": ";
122
      pol.d_kind = DNSFilterEngine::PolicyKind::NXDOMAIN;
18✔
123
    }
18✔
124
    else if (crcTarget == g_wildcarddnsname) {
39✔
125
      // cerr<<"Wants NODATA for "<<dr.d_name<<": ";
126
      pol.d_kind = DNSFilterEngine::PolicyKind::NODATA;
4✔
127
    }
4✔
128
    else if (crcTarget == drop) {
35✔
129
      // cerr<<"Wants DROP for "<<dr.d_name<<": ";
130
      pol.d_kind = DNSFilterEngine::PolicyKind::Drop;
5✔
131
    }
5✔
132
    else if (crcTarget == truncate) {
30✔
133
      // cerr<<"Wants TRUNCATE for "<<dr.d_name<<": ";
134
      pol.d_kind = DNSFilterEngine::PolicyKind::Truncate;
5✔
135
    }
5✔
136
    else if (crcTarget == noaction) {
25✔
137
      // cerr<<"Wants NOACTION for "<<dr.d_name<<": ";
138
      pol.d_kind = DNSFilterEngine::PolicyKind::NoAction;
17✔
139
    }
17✔
140
    /* "The special RPZ encodings which are not to be taken as Local Data are
141
       CNAMEs with targets that are:
142
       +  "."  (NXDOMAIN action),
143
       +  "*." (NODATA action),
144
       +  a top level domain starting with "rpz-",
145
       +  a child of a top level domain starting with "rpz-".
146
    */
147
    else if (!crcTarget.empty() && !crcTarget.isRoot() && crcTarget.getRawLabel(crcTarget.countLabels() - 1).compare(0, rpzPrefix.length(), rpzPrefix) == 0) {
8!
148
      /* this is very likely an higher format number or a configuration error,
149
         let's just ignore it. */
150
      SLOG(g_log << Logger::Info << "Discarding unsupported RPZ entry " << crcTarget << " for " << dnsRecord.d_name << endl,
×
151
           log->info(Logr::Info, "Discarding unsupported RPZ entry", "target", Logging::Loggable(crcTarget), "name", Logging::Loggable(dnsRecord.d_name)));
×
152
      return;
×
153
    }
×
154
    else {
8✔
155
      pol.d_kind = DNSFilterEngine::PolicyKind::Custom;
8✔
156
      if (!pol.d_custom) {
8!
157
        pol.d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
8✔
158
      }
8✔
159
      pol.d_custom->emplace_back(dnsRecord.getContent());
8✔
160
      // cerr<<"Wants custom "<<crcTarget<<" for "<<dr.d_name<<": ";
161
    }
8✔
162
  }
61✔
163
  else {
59✔
164
    if (defpol && defpolOverrideLocal) {
59✔
165
      pol = *defpol;
2✔
166
      defpolApplied = true;
2✔
167
    }
2✔
168
    else {
57✔
169
      pol.d_kind = DNSFilterEngine::PolicyKind::Custom;
57✔
170
      if (!pol.d_custom) {
57!
171
        pol.d_custom = make_unique<DNSFilterEngine::Policy::CustomData>();
57✔
172
      }
57✔
173
      pol.d_custom->emplace_back(dnsRecord.getContent());
57✔
174
      // cerr<<"Wants custom "<<dr.d_content->getZoneRepresentation()<<" for "<<dr.d_name<<": ";
175
    }
57✔
176
  }
59✔
177

178
  if (!defpolApplied || defpol->d_ttl < 0) {
120!
179
    pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, dnsRecord.d_ttl));
114✔
180
  }
114✔
181
  else {
6✔
182
    pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, static_cast<uint32_t>(pol.d_ttl)));
6✔
183
  }
6✔
184

185
  // now to DO something with that
186

187
  if (dnsRecord.d_name.isPartOf(rpzNSDname)) {
120✔
188
    DNSName filt = dnsRecord.d_name.makeRelative(rpzNSDname);
2✔
189
    if (addOrRemove) {
2!
190
      zone->addNSTrigger(filt, std::move(pol), defpolApplied);
2✔
191
    }
2✔
192
    else {
×
193
      zone->rmNSTrigger(filt, pol);
×
194
    }
×
195
  }
2✔
196
  else if (dnsRecord.d_name.isPartOf(rpzClientIP)) {
118!
197
    DNSName filt = dnsRecord.d_name.makeRelative(rpzClientIP);
×
198
    auto netmask = makeNetmaskFromRPZ(filt);
×
199
    if (addOrRemove) {
×
200
      zone->addClientTrigger(netmask, std::move(pol), defpolApplied);
×
201
    }
×
202
    else {
×
203
      zone->rmClientTrigger(netmask, pol);
×
204
    }
×
205
  }
×
206
  else if (dnsRecord.d_name.isPartOf(rpzIP)) {
118✔
207
    // cerr<<"Should apply answer content IP policy: "<<dr.d_name<<endl;
208
    DNSName filt = dnsRecord.d_name.makeRelative(rpzIP);
26✔
209
    auto netmask = makeNetmaskFromRPZ(filt);
26✔
210
    if (addOrRemove) {
26!
211
      zone->addResponseTrigger(netmask, std::move(pol), defpolApplied);
26✔
212
    }
26✔
213
    else {
×
214
      zone->rmResponseTrigger(netmask, pol);
×
215
    }
×
216
  }
26✔
217
  else if (dnsRecord.d_name.isPartOf(rpzNSIP)) {
92✔
218
    DNSName filt = dnsRecord.d_name.makeRelative(rpzNSIP);
4✔
219
    auto netmask = makeNetmaskFromRPZ(filt);
4✔
220
    if (addOrRemove) {
4!
221
      zone->addNSIPTrigger(netmask, std::move(pol), defpolApplied);
4✔
222
    }
4✔
223
    else {
×
224
      zone->rmNSIPTrigger(netmask, pol);
×
225
    }
×
226
  }
4✔
227
  else {
88✔
228
    if (addOrRemove) {
88✔
229
      /* if we did override the existing policy with the default policy,
230
         we might turn two A or AAAA into a CNAME, which would trigger
231
         an exception. Let's just ignore it. */
232
      zone->addQNameTrigger(dnsRecord.d_name, std::move(pol), defpolApplied);
80✔
233
    }
80✔
234
    else {
8✔
235
      zone->rmQNameTrigger(dnsRecord.d_name, pol);
8✔
236
    }
8✔
237
  }
88✔
238
}
120✔
239

240
static shared_ptr<const SOARecordContent> loadRPZFromServer(Logr::log_t plogger, const ComboAddress& primary, const DNSName& zoneName, const std::shared_ptr<DNSFilterEngine::Zone>& zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, const TSIGTriplet& tsigTriplet, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout)
241
{
4✔
242

243
  auto logger = plogger->withValues("primary", Logging::Loggable(primary));
4✔
244
  SLOG(g_log << Logger::Warning << "Loading RPZ zone '" << zoneName << "' from " << primary.toStringWithPort() << endl,
4✔
245
       logger->info(Logr::Info, "Loading RPZ from nameserver"));
4✔
246
  if (!tsigTriplet.name.empty()) {
4!
247
    SLOG(g_log << Logger::Warning << "With TSIG key '" << tsigTriplet.name << "' of algorithm '" << tsigTriplet.algo << "'" << endl,
×
248
         logger->info(Logr::Info, "Using TSIG key for authentication", "tsig_key_name", Logging::Loggable(tsigTriplet.name), "tsig_key_algorithm", Logging::Loggable(tsigTriplet.algo)));
×
249
  }
×
250

251
  ComboAddress local(localAddress);
4✔
252
  if (local == ComboAddress()) {
4!
253
    local = pdns::getQueryLocalAddress(primary.sin4.sin_family, 0);
4✔
254
  }
4✔
255

256
  AXFRRetriever axfr(primary, zoneName, tsigTriplet, &local, maxReceivedBytes, axfrTimeout);
4✔
257
  unsigned int nrecords = 0;
4✔
258
  Resolver::res_t nop;
4✔
259
  vector<DNSRecord> chunk;
4✔
260
  time_t last = 0;
4✔
261
  time_t axfrStart = time(nullptr);
4✔
262
  time_t axfrNow = time(nullptr);
4✔
263
  shared_ptr<const SOARecordContent> soaRecordContent;
4✔
264
  // coverity[store_truncates_time_t]
265
  while (axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow)) != 0) {
6✔
266
    for (auto& dnsRecord : chunk) {
6✔
267
      if (dnsRecord.d_type == QType::NS || dnsRecord.d_type == QType::TSIG) {
6!
268
        continue;
×
269
      }
×
270

271
      // We want the full name in the SOA record
272
      if (dnsRecord.d_type == QType::SOA) {
6✔
273
        zone->setSOA(dnsRecord);
4✔
274
      }
4✔
275
      dnsRecord.d_name.makeUsRelative(zoneName);
6✔
276
      if (dnsRecord.d_type == QType::SOA) {
6✔
277
        soaRecordContent = getRR<SOARecordContent>(dnsRecord);
4✔
278
        continue;
4✔
279
      }
4✔
280

281
      RPZRecordToPolicy(dnsRecord, zone, true, defpol, defpolOverrideLocal, maxTTL, logger);
2✔
282
      nrecords++;
2✔
283
    }
2✔
284
    axfrNow = time(nullptr);
2✔
285
    if (axfrNow < axfrStart || axfrNow - axfrStart > axfrTimeout) {
2!
286
      throw PDNSException("Total AXFR time exceeded!");
×
287
    }
×
288
    if (last != time(nullptr)) {
2!
289
      SLOG(g_log << Logger::Info << "Loaded & indexed " << nrecords << " policy records so far for RPZ zone '" << zoneName << "'" << endl,
2✔
290
           logger->info(Logr::Info, "RPZ load in progress", "nrecords", Logging::Loggable(nrecords)));
2✔
291
      last = time(nullptr);
2✔
292
    }
2✔
293
  }
2✔
294
  SLOG(g_log << Logger::Info << "Done: " << nrecords << " policy records active, SOA: " << soaRecordContent->getZoneRepresentation() << endl,
4✔
295
       logger->info(Logr::Info, "RPZ load completed", "nrecords", Logging::Loggable(nrecords), "soa", Logging::Loggable(soaRecordContent->getZoneRepresentation())));
4✔
296
  return soaRecordContent;
4✔
297
}
4✔
298

299
static LockGuarded<std::unordered_map<std::string, shared_ptr<rpzStats>>> s_rpzStats;
300

301
shared_ptr<rpzStats> getRPZZoneStats(const std::string& zone)
302
{
69✔
303
  auto stats = s_rpzStats.lock();
69✔
304
  auto statsIt = stats->find(zone);
69✔
305
  if (statsIt == stats->end()) {
69✔
306
    auto stat = std::make_shared<rpzStats>();
19✔
307
    (*stats)[zone] = stat;
19✔
308
    return stat;
19✔
309
  }
19✔
310
  return statsIt->second;
50✔
311
}
69✔
312

313
static void incRPZFailedTransfers(const std::string& zone)
314
{
26✔
315
  auto stats = getRPZZoneStats(zone);
26✔
316
  if (stats != nullptr) {
26!
317
    stats->d_failedTransfers++;
26✔
318
  }
26✔
319
}
26✔
320

321
static void setRPZZoneNewState(const std::string& zone, uint32_t serial, uint64_t numberOfRecords, bool fromFile, bool wasAXFR)
322
{
30✔
323
  auto stats = getRPZZoneStats(zone);
30✔
324
  if (stats == nullptr) {
30!
325
    return;
×
326
  }
×
327
  if (!fromFile) {
30✔
328
    stats->d_successfulTransfers++;
11✔
329
    if (wasAXFR) {
11✔
330
      stats->d_fullTransfers++;
5✔
331
    }
5✔
332
  }
11✔
333
  stats->d_lastUpdate = time(nullptr);
30✔
334
  stats->d_serial = serial;
30✔
335
  stats->d_numberOfRecords = numberOfRecords;
30✔
336
}
30✔
337

338
// this function is silent - you do the logging
339
std::shared_ptr<const SOARecordContent> loadRPZFromFile(const std::string& fname, const std::shared_ptr<DNSFilterEngine::Zone>& zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL)
340
{
21✔
341
  shared_ptr<const SOARecordContent> soaRecordContent = nullptr;
21✔
342
  ZoneParserTNG zpt(fname);
21✔
343
  zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
21✔
344
  zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
21✔
345
  DNSResourceRecord drr;
21✔
346
  DNSRecord soaRecord;
21✔
347
  DNSName domain;
21✔
348
  auto log = g_slog->withName("rpz")->withValues("file", Logging::Loggable(fname), "zone", Logging::Loggable(zone->getName()));
21✔
349
  while (zpt.get(drr)) {
138✔
350
    try {
119✔
351
      if (drr.qtype.getCode() == QType::CNAME && drr.content.empty()) {
119!
352
        drr.content = ".";
×
353
      }
×
354
      DNSRecord dnsRecord(drr);
119✔
355
      if (dnsRecord.d_type == QType::SOA) {
119✔
356
        soaRecordContent = getRR<SOARecordContent>(dnsRecord);
21✔
357
        domain = dnsRecord.d_name;
21✔
358
        zone->setDomain(domain);
21✔
359
        soaRecord = std::move(dnsRecord);
21✔
360
      }
21✔
361
      else if (dnsRecord.d_type == QType::NS) {
98✔
362
        continue;
6✔
363
      }
6✔
364
      else {
92✔
365
        dnsRecord.d_name = dnsRecord.d_name.makeRelative(domain);
92✔
366
        RPZRecordToPolicy(dnsRecord, zone, true, defpol, defpolOverrideLocal, maxTTL, log);
92✔
367
      }
92✔
368
    }
119✔
369
    catch (const PDNSException& pe) {
119✔
370
      throw PDNSException("Issue parsing '" + drr.qname.toLogString() + "' '" + drr.content + "' at " + zpt.getLineOfFile() + ": " + pe.reason);
×
371
    }
×
372
  }
119✔
373

374
  if (soaRecordContent != nullptr) {
19!
375
    zone->setRefresh(soaRecordContent->d_st.refresh);
19✔
376
    zone->setSOA(std::move(soaRecord));
19✔
377
    setRPZZoneNewState(zone->getName(), soaRecordContent->d_st.serial, zone->size(), true, false);
19✔
378
  }
19✔
379
  return soaRecordContent;
19✔
380
}
21✔
381

382
struct FilenameDeleter
383
{
384
  void operator()(const string* name) const noexcept
385
  {
10✔
386
    if (name != nullptr) {
10!
387
      if (!name->empty()) {
10!
388
        unlink(name->c_str());
×
389
      }
×
390
      delete name; // NOLINT(cppcoreguidelines-owning-memory)
10✔
391
    }
10✔
392
  }
10✔
393
};
394

395
using UniqueFilenameDeleterPtr = std::unique_ptr<std::string, FilenameDeleter>;
396

397
static bool dumpZoneToDisk(Logr::log_t logger, const std::shared_ptr<DNSFilterEngine::Zone>& newZone, const std::string& dumpZoneFileName)
398
{
10✔
399
  logger->info(Logr::Debug, "Dumping zone to disk", "destination_file", Logging::Loggable(dumpZoneFileName));
10✔
400
  DNSRecord soa = newZone->getSOA();
10✔
401
  uint32_t serial = 0;
10✔
402
  DNSName zone;
10✔
403
  if (auto soaContent = getRR<SOARecordContent>(soa)) {
10!
404
    serial = soaContent->d_st.serial;
10✔
405
  }
10✔
406
  if (newZone->getSerial() != serial) {
10!
407
    logger->info(Logr::Error, "Inconsistency of internal serial and SOA serial", "serial", Logging::Loggable(newZone->getSerial()), "soaserial", Logging::Loggable(serial));
×
408
  }
×
409

410
  if (newZone->getDomain() != soa.d_name) {
10!
411
    logger->info(Logr::Error, "Inconsistency of internal name and SOA name", "zone", Logging::Loggable(newZone->getDomain()), "soaname", Logging::Loggable(soa.d_name));
×
412
  }
×
413
  auto tempFile = UniqueFilenameDeleterPtr(new string(dumpZoneFileName + "XXXXXX"));
10✔
414
  int fileDesc = mkstemp(tempFile->data());
10✔
415
  if (fileDesc < 0) {
10!
416
    SLOG(g_log << Logger::Warning << "Unable to open a file to dump the content of the RPZ zone " << zoneName << endl,
×
417
         logger->error(Logr::Error, errno, "Unable to create temporary file"));
×
418
    tempFile->clear(); // file has not been created, no need to unlink
×
419
    return false;
×
420
  }
×
421

422
  auto filePtr = pdns::UniqueFilePtr(fdopen(fileDesc, "w+"));
10✔
423
  if (!filePtr) {
10!
424
    int err = errno;
×
425
    close(fileDesc);
×
426
    SLOG(g_log << Logger::Warning << "Unable to open a file pointer to dump the content of the RPZ zone " << zoneName << endl,
×
427
         logger->error(Logr::Error, err, "Unable to open file pointer"));
×
428
    return false;
×
429
  }
×
430

431
  try {
10✔
432
    newZone->dump(filePtr.get());
10✔
433
  }
10✔
434
  catch (const std::exception& e) {
10✔
435
    SLOG(g_log << Logger::Warning << "Error while dumping the content of the RPZ zone " << zoneName << ": " << e.what() << endl,
×
436
         logger->error(Logr::Error, e.what(), "Error while dumping the content of the RPZ"));
×
437
    return false;
×
438
  }
×
439

440
  if (fflush(filePtr.get()) != 0) {
10!
441
    SLOG(g_log << Logger::Warning << "Error while flushing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
×
442
         logger->error(Logr::Warning, errno, "Error while flushing the content of the RPZ"));
×
443
    return false;
×
444
  }
×
445

446
  if (fsync(fileno(filePtr.get())) != 0) {
10!
447
    SLOG(g_log << Logger::Warning << "Error while syncing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
×
448
         logger->error(Logr::Error, errno, "Error while syncing the content of the RPZ"));
×
449
    return false;
×
450
  }
×
451

452
  if (fclose(filePtr.release()) != 0) {
10!
453
    SLOG(g_log << Logger::Warning << "Error while writing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
×
454
         logger->error(Logr::Error, errno, "Error while writing the content of the RPZ"));
×
455
    return false;
×
456
  }
×
457

458
  if (rename(tempFile->c_str(), dumpZoneFileName.c_str()) != 0) {
10!
459
    SLOG(g_log << Logger::Warning << "Error while moving the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl,
×
460
         logger->error(Logr::Error, errno, "Error while moving the content of the RPZ", "destination_file", Logging::Loggable(dumpZoneFileName)));
×
461
    return false;
×
462
  }
×
463
  tempFile->clear(); // file has been renamed, no need to unlink
10✔
464
  return true;
10✔
465
}
10✔
466

467
static void preloadRPZFIle(RPZTrackerParams& params, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone>& oldZone, uint32_t& refresh, const string& polName, uint64_t configGeneration, ZoneXFR::ZoneWaiter& rpzwaiter, Logr::log_t logger)
468
{
2✔
469
  while (!params.zoneXFRParams.soaRecordContent) {
6✔
470
    /* if we received an empty sr, the zone was not really preloaded */
471

472
    /* full copy, as promised */
473
    std::shared_ptr<DNSFilterEngine::Zone> newZone = std::make_shared<DNSFilterEngine::Zone>(*oldZone);
2✔
474
    for (const auto& primary : params.zoneXFRParams.primaries) {
4!
475
      try {
4✔
476
        auto combo = pdns::fromNameOrIP(primary, 53, logger);
4✔
477
        params.zoneXFRParams.soaRecordContent = loadRPZFromServer(logger, combo, zoneName, newZone, params.defpol, params.defpolOverrideLocal, params.maxTTL, params.zoneXFRParams.tsigtriplet, params.zoneXFRParams.maxReceivedMBytes, params.zoneXFRParams.localAddress, params.zoneXFRParams.xfrTimeout);
4✔
478
        newZone->setSerial(params.zoneXFRParams.soaRecordContent->d_st.serial);
4✔
479
        newZone->setRefresh(params.zoneXFRParams.soaRecordContent->d_st.refresh);
4✔
480
        refresh = std::max(params.zoneXFRParams.refreshFromConf != 0 ? params.zoneXFRParams.refreshFromConf : newZone->getRefresh(), 1U);
4✔
481
        setRPZZoneNewState(polName, params.zoneXFRParams.soaRecordContent->d_st.serial, newZone->size(), false, true);
4✔
482

483
        g_luaconfs.modify([zoneIdx = params.zoneXFRParams.zoneIdx, &newZone](LuaConfigItems& lci) {
4✔
484
          lci.dfe.setZone(zoneIdx, newZone);
2✔
485
        });
2✔
486

487
        if (!params.dumpZoneFileName.empty()) {
4✔
488
          dumpZoneToDisk(logger, newZone, params.dumpZoneFileName);
1✔
489
        }
1✔
490

491
        /* no need to try another primary */
492
        break;
4✔
493
      }
4✔
494
      catch (const std::exception& e) {
4✔
495
        SLOG(g_log << Logger::Warning << "Unable to load RPZ zone '" << zoneName << "' from '" << primary << "': '" << e.what() << "'. (Will try again in " << refresh << " seconds...)" << endl,
×
496
             logger->error(Logr::Warning, e.what(), "Unable to load RPZ zone, will retry", "from", Logging::Loggable(primary), "exception", Logging::Loggable("std::exception"), "refresh", Logging::Loggable(refresh)));
×
497
        incRPZFailedTransfers(polName);
×
498
      }
×
499
      catch (const PDNSException& e) {
4✔
500
        SLOG(g_log << Logger::Warning << "Unable to load RPZ zone '" << zoneName << "' from '" << primary << "': '" << e.reason << "'. (Will try again in " << refresh << " seconds...)" << endl,
2✔
501
             logger->error(Logr::Warning, e.reason, "Unable to load RPZ zone, will retry", "from", Logging::Loggable(primary), "exception", Logging::Loggable("PDNSException"), "refresh", Logging::Loggable(refresh)));
2✔
502
        incRPZFailedTransfers(polName);
2✔
503
      }
2✔
504
    }
4✔
505
    // Release newZone before (long) sleep to reduce memory usage
506
    newZone = nullptr;
4✔
507
    if (!params.zoneXFRParams.soaRecordContent) {
4!
508
      std::unique_lock lock(rpzwaiter.mutex);
×
509
      rpzwaiter.condVar.wait_for(lock, std::chrono::seconds(refresh),
×
510
                                 [&stop = rpzwaiter.stop] { return stop.load(); });
×
511
    }
×
512
    rpzwaiter.stop = false;
4✔
513
    auto luaconfsLocal = g_luaconfs.getLocal();
4✔
514

515
    if (luaconfsLocal->generation != configGeneration) {
4!
516
      /* the configuration has been reloaded, meaning that a new thread
517
         has been started to handle that zone and we are now obsolete.
518
      */
519
      return;
×
520
    }
×
521
  }
4✔
522
}
2✔
523

524
static bool RPZTrackerIteration(RPZTrackerParams& params, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone>& oldZone, uint32_t& refresh, const string& polName, bool& skipRefreshDelay, uint64_t configGeneration, ZoneXFR::ZoneWaiter& rpzwaiter, Logr::log_t logger)
525
{
21✔
526
  // Don't hold on to oldZone, it well be re-assigned after sleep in the try block
527
  oldZone = nullptr;
21✔
528
  DNSRecord dnsRecord;
21✔
529
  dnsRecord.setContent(params.zoneXFRParams.soaRecordContent);
21✔
530

531
  if (skipRefreshDelay) {
21!
532
    skipRefreshDelay = false;
×
533
  }
×
534
  else {
21✔
535
    const time_t minimumTimeBetweenRefreshes = std::min(refresh, 5U);
21✔
536
    const time_t startTime = time(nullptr);
21✔
537
    time_t wakeTime = startTime;
21✔
538
    while (wakeTime - startTime < minimumTimeBetweenRefreshes) {
49✔
539
      std::unique_lock lock(rpzwaiter.mutex);
28✔
540
      time_t remaining = refresh - (wakeTime - startTime);
28✔
541
      if (remaining <= 0) {
28!
542
        break;
×
543
      }
×
544
      rpzwaiter.condVar.wait_for(lock, std::chrono::seconds(remaining),
28✔
545
                                 [&stop = rpzwaiter.stop] { return stop.load(); });
54✔
546
      rpzwaiter.stop = false;
28✔
547
      wakeTime = time(nullptr);
28✔
548
    }
28✔
549
  }
21✔
550
  auto luaconfsLocal = g_luaconfs.getLocal();
21✔
551

552
  if (luaconfsLocal->generation != configGeneration) {
21!
553
    /* the configuration has been reloaded, meaning that a new thread
554
       has been started to handle that zone and we are now obsolete.
555
    */
556
    SLOG(g_log << Logger::Info << "A more recent configuration has been found, stopping the existing RPZ update thread for " << zoneName << endl,
×
557
         logger->info(Logr::Info, "A more recent configuration has been found, stopping the existing RPZ update thread"));
×
558
    return false;
×
559
  }
×
560

561
  vector<pair<vector<DNSRecord>, vector<DNSRecord>>> deltas;
21✔
562
  for (const auto& ipOrName : params.zoneXFRParams.primaries) {
38✔
563
    auto primary = pdns::fromNameOrIP(ipOrName, 53, logger);
38✔
564
    auto soa = getRR<SOARecordContent>(dnsRecord);
38✔
565
    auto serial = soa ? soa->d_st.serial : 0;
38!
566
    SLOG(g_log << Logger::Info << "Getting IXFR deltas for " << zoneName << " from " << primary.toStringWithPort() << ", our serial: " << serial << endl,
38✔
567
         logger->info(Logr::Info, "Getting IXFR deltas", "address", Logging::Loggable(primary), "ourserial", Logging::Loggable(serial)));
38✔
568

569
    ComboAddress local(params.zoneXFRParams.localAddress);
38✔
570
    if (local == ComboAddress()) {
38!
571
      local = pdns::getQueryLocalAddress(primary.sin4.sin_family, 0);
38✔
572
    }
38✔
573

574
    try {
38✔
575
      deltas = getIXFRDeltas(primary, zoneName, dnsRecord, params.zoneXFRParams.xfrTimeout, true, params.zoneXFRParams.tsigtriplet, &local, params.zoneXFRParams.maxReceivedMBytes);
38✔
576

577
      /* no need to try another primary */
578
      break;
38✔
579
    }
38✔
580
    catch (const std::runtime_error& e) {
38✔
581
      SLOG(g_log << Logger::Warning << e.what() << endl,
24✔
582
           logger->error(Logr::Warning, e.what(), "Exception during retrieval of delta", "exception", Logging::Loggable("std::runtime_error")));
24✔
583
      incRPZFailedTransfers(polName);
24✔
584
      continue;
24✔
585
    }
24✔
586
  }
38✔
587

588
  if (deltas.empty()) {
45✔
589
    return true;
8✔
590
  }
8✔
591

592
  try {
37✔
593
    SLOG(g_log << Logger::Info << "Processing " << deltas.size() << " delta" << addS(deltas) << " for RPZ " << zoneName << endl,
37✔
594
         logger->info(Logr::Info, "Processing deltas", "size", Logging::Loggable(deltas.size())));
37✔
595

596
    if (luaconfsLocal->generation != configGeneration) {
37!
597
      SLOG(g_log << Logger::Info << "A more recent configuration has been found, stopping the existing RPZ update thread for " << zoneName << endl,
×
598
           logger->info(Logr::Info, "A more recent configuration has been found, stopping the existing RPZ update thread"));
×
599
      return false;
×
600
    }
×
601
    oldZone = luaconfsLocal->dfe.getZone(params.zoneXFRParams.zoneIdx);
37✔
602
    if (!oldZone || oldZone->getDomain() != zoneName) {
37!
603
      SLOG(g_log << Logger::Info << "This policy is no more, stopping the existing RPZ update thread for " << zoneName << endl,
×
604
           logger->info(Logr::Info, "This policy is no more, stopping the existing RPZ update thread"));
×
605
      return false;
×
606
    }
×
607
    /* we need to make a _full copy_ of the zone we are going to work on */
608
    std::shared_ptr<DNSFilterEngine::Zone> newZone = std::make_shared<DNSFilterEngine::Zone>(*oldZone);
37✔
609
    /* initialize the current serial to the last one */
610
    std::shared_ptr<const SOARecordContent> currentSR = params.zoneXFRParams.soaRecordContent;
37✔
611

612
    int totremove = 0;
37✔
613
    int totadd = 0;
37✔
614
    bool fullUpdate = false;
37✔
615
    for (const auto& delta : deltas) {
37✔
616
      const auto& remove = delta.first;
12✔
617
      const auto& add = delta.second;
12✔
618
      if (remove.empty()) {
12✔
619
        SLOG(g_log << Logger::Warning << "IXFR update is a whole new zone" << endl,
3✔
620
             logger->info(Logr::Warning, "IXFR update is a whole new zone"));
3✔
621
        newZone->clear();
3✔
622
        fullUpdate = true;
3✔
623
      }
3✔
624
      for (const auto& resourceRecord : remove) { // should always contain the SOA
17✔
625
        if (resourceRecord.d_type == QType::NS) {
17!
626
          continue;
×
627
        }
×
628
        if (resourceRecord.d_type == QType::SOA) {
17✔
629
          auto oldsr = getRR<SOARecordContent>(resourceRecord);
9✔
630
          if (oldsr && oldsr->d_st.serial == currentSR->d_st.serial) {
9!
631
            //        Got good removal of SOA serial, no work to be done
632
          }
9✔
633
          else {
×
634
            if (!oldsr) {
×
635
              throw std::runtime_error("Unable to extract serial from SOA record while processing the removal part of an update");
×
636
            }
×
637
            throw std::runtime_error("Received an unexpected serial (" + std::to_string(oldsr->d_st.serial) + ", expecting " + std::to_string(currentSR->d_st.serial) + ") from SOA record while processing the removal part of an update");
×
638
          }
×
639
        }
9✔
640
        else {
8✔
641
          totremove++;
8✔
642
          SLOG(g_log << (g_logRPZChanges ? Logger::Info : Logger::Debug) << "Had removal of " << resourceRecord.d_name << " from RPZ zone " << zoneName << endl,
8!
643
               logger->info(g_logRPZChanges ? Logr::Info : Logr::Debug, "Remove from RPZ zone", "name", Logging::Loggable(resourceRecord.d_name)));
8✔
644
          RPZRecordToPolicy(resourceRecord, newZone, false, params.defpol, params.defpolOverrideLocal, params.maxTTL, logger);
8✔
645
        }
8✔
646
      }
17✔
647

648
      for (const auto& resourceRecord : add) { // should always contain the new SOA
33✔
649
        if (resourceRecord.d_type == QType::NS) {
33!
650
          continue;
×
651
        }
×
652
        if (resourceRecord.d_type == QType::SOA) {
33✔
653
          if (auto tempSR = getRR<SOARecordContent>(resourceRecord)) {
15!
654
            dnsRecord = resourceRecord;
15✔
655
            // IXFR leaves us a relative name, fix that
656
            dnsRecord.d_name = newZone->getDomain();
15✔
657
            currentSR = std::move(tempSR);
15✔
658
          }
15✔
659
        }
15✔
660
        else {
18✔
661
          totadd++;
18✔
662
          SLOG(g_log << (g_logRPZChanges ? Logger::Info : Logger::Debug) << "Had addition of " << resourceRecord.d_name << " to RPZ zone " << zoneName << endl,
18!
663
               logger->info(g_logRPZChanges ? Logr::Info : Logr::Debug, "Addition to RPZ zone", "name", Logging::Loggable(resourceRecord.d_name)));
18✔
664
          RPZRecordToPolicy(resourceRecord, newZone, true, params.defpol, params.defpolOverrideLocal, params.maxTTL, logger);
18✔
665
        }
18✔
666
      }
33✔
667
    }
12✔
668

669
    /* only update sr now that all changes have been converted */
670
    if (currentSR) {
37✔
671
      newZone->setSOA(std::move(dnsRecord));
9✔
672
      params.zoneXFRParams.soaRecordContent = std::move(currentSR);
9✔
673
    }
9✔
674
    SLOG(g_log << Logger::Info << "Had " << totremove << " RPZ removal" << addS(totremove) << ", " << totadd << " addition" << addS(totadd) << " for " << zoneName << " New serial: " << params.soaRecordContent->d_st.serial << endl,
37✔
675
         logger->info(Logr::Info, "RPZ mutations", "removals", Logging::Loggable(totremove), "additions", Logging::Loggable(totadd), "newserial", Logging::Loggable(params.zoneXFRParams.soaRecordContent->d_st.serial)));
37✔
676
    newZone->setSerial(params.zoneXFRParams.soaRecordContent->d_st.serial);
37✔
677
    newZone->setRefresh(params.zoneXFRParams.soaRecordContent->d_st.refresh);
37✔
678
    setRPZZoneNewState(polName, params.zoneXFRParams.soaRecordContent->d_st.serial, newZone->size(), false, fullUpdate);
37✔
679

680
    /* we need to replace the existing zone with the new one,
681
       but we don't want to touch anything else, especially other zones,
682
       since they might have been updated by another RPZ IXFR tracker thread.
683
    */
684
    if (luaconfsLocal->generation != configGeneration) {
37!
685
      SLOG(g_log << Logger::Info << "A more recent configuration has been found, stopping the existing RPZ update thread for " << zoneName << endl,
×
686
           logger->info(Logr::Info, "A more recent configuration has been found, stopping the existing RPZ update thread"));
×
687
      return false;
×
688
    }
×
689
    g_luaconfs.modify([zoneIdx = params.zoneXFRParams.zoneIdx, &newZone](LuaConfigItems& lci) {
37✔
690
      lci.dfe.setZone(zoneIdx, newZone);
9✔
691
    });
9✔
692

693
    if (!params.dumpZoneFileName.empty()) {
37✔
694
      dumpZoneToDisk(logger, newZone, params.dumpZoneFileName);
9✔
695
    }
9✔
696
    refresh = std::max(params.zoneXFRParams.refreshFromConf != 0 ? params.zoneXFRParams.refreshFromConf : newZone->getRefresh(), 1U);
37✔
697
  }
37✔
698
  catch (const std::exception& e) {
37✔
699
    SLOG(g_log << Logger::Error << "Error while applying the update received over XFR for " << zoneName << ", skipping the update: " << e.what() << endl,
2✔
700
         logger->error(Logr::Error, e.what(), "Exception while applying the update received over XFR, skipping", "exception", Logging::Loggable("std::exception")));
2✔
701
  }
2✔
702
  catch (const PDNSException& e) {
37✔
703
    SLOG(g_log << Logger::Error << "Error while applying the update received over XFR for " << zoneName << ", skipping the update: " << e.reason << endl,
×
704
         logger->error(Logr::Error, e.reason, "Exception while applying the update received over XFR, skipping", "exception", Logging::Loggable("PDNSException")));
×
705
  }
×
706
  return true;
11✔
707
}
37✔
708

709
// coverity[pass_by_value] params is intended to be a copy, as this is the main function of a thread
710
void RPZIXFRTracker(RPZTrackerParams params, uint64_t configGeneration)
711
{
2✔
712
  setThreadName("rec/rpzixfr");
2✔
713
  bool isPreloaded = params.zoneXFRParams.soaRecordContent != nullptr;
2✔
714
  auto logger = g_slog->withName("rpz");
2✔
715
  ZoneXFR::ZoneWaiter waiter(std::this_thread::get_id());
2✔
716

717
  /* we can _never_ modify this zone directly, we need to do a full copy then replace the existing zone */
718
  std::shared_ptr<DNSFilterEngine::Zone> oldZone = g_luaconfs.getLocal()->dfe.getZone(params.zoneXFRParams.zoneIdx);
2✔
719
  if (!oldZone) {
2!
720
    SLOG(g_log << Logger::Error << "Unable to retrieve RPZ zone with index " << params.zoneIdx << " from the configuration, exiting" << endl,
×
721
         logger->error(Logr::Error, "Unable to retrieve RPZ zone from configuration", "index", Logging::Loggable(params.zoneXFRParams.zoneIdx)));
×
722
    return;
×
723
  }
×
724

725
  // If oldZone failed to load its getRefresh() returns 0, protect against that
726
  uint32_t refresh = std::max(params.zoneXFRParams.refreshFromConf != 0 ? params.zoneXFRParams.refreshFromConf : oldZone->getRefresh(), 10U);
2!
727
  DNSName zoneName = oldZone->getDomain();
2✔
728
  std::string polName = !oldZone->getName().empty() ? oldZone->getName() : zoneName.toStringNoDot();
2!
729

730
  // Now that we know the name, set it in the logger
731
  logger = logger->withValues("zone", Logging::Loggable(zoneName));
2✔
732

733
  ZoneXFR::insertZoneTracker(zoneName, waiter);
2✔
734

735
  preloadRPZFIle(params, zoneName, oldZone, refresh, polName, configGeneration, waiter, logger);
2✔
736

737
  bool skipRefreshDelay = isPreloaded;
2✔
738

739
  while (RPZTrackerIteration(params, zoneName, oldZone, refresh, polName, skipRefreshDelay, configGeneration, waiter, logger)) {
21✔
740
    // empty
741
  }
19✔
742

743
  ZoneXFR::clearZoneTracker(zoneName);
2✔
744
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc