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

PowerDNS / pdns / 15920796590

27 Jun 2025 07:31AM UTC coverage: 65.647% (+0.03%) from 65.614%
15920796590

Pull #15733

github

web-flow
Merge f99cd2479 into a44ba546f
Pull Request #15733: Reduce ZoneName::toString mess

41569 of 91856 branches covered (45.25%)

Branch coverage included in aggregate %.

86 of 120 new or added lines in 8 files covered. (71.67%)

53 existing lines in 12 files now uncovered.

126844 of 164688 relevant lines covered (77.02%)

5123086.44 hits per line

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

64.47
/pdns/auth-secondarycommunicator.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
#ifdef HAVE_CONFIG_H
23
#include "config.h"
24
#endif
25

26
#include "utility.hh"
27
#include "dnssecinfra.hh"
28
#include "dnsseckeeper.hh"
29
#include "base32.hh"
30
#include <cerrno>
31
#include "communicator.hh"
32
#include <set>
33
#include <boost/utility.hpp>
34
#include "dnsbackend.hh"
35
#include "ueberbackend.hh"
36
#include "packethandler.hh"
37
#include "axfr-retriever.hh"
38
#include "logger.hh"
39
#include "dns.hh"
40
#include "arguments.hh"
41
#include "auth-caches.hh"
42

43
#include "base64.hh"
44
#include "inflighter.cc"
45
#include "namespaces.hh"
46
#include "auth-main.hh"
47
#include "query-local-address.hh"
48

49
#include "ixfr.hh"
50

51
void CommunicatorClass::addSuckRequest(const ZoneName& domain, const ComboAddress& primary, SuckRequest::RequestPriority priority, bool force)
52
{
452✔
53
  auto data = d_data.lock();
452✔
54
  SuckRequest sr;
452✔
55
  sr.domain = domain;
452✔
56
  sr.primary = primary;
452✔
57
  sr.force = force;
452✔
58
  sr.priorityAndOrder.first = priority;
452✔
59
  sr.priorityAndOrder.second = data->d_sorthelper++;
452✔
60
  pair<UniQueue::iterator, bool> res;
452✔
61

62
  res = data->d_suckdomains.insert(sr);
452✔
63
  if (res.second) {
452!
64
    d_suck_sem.post();
452✔
65
  }
452✔
66
  else {
×
67
    data->d_suckdomains.modify(res.first, [priorityAndOrder = sr.priorityAndOrder](SuckRequest& so) {
×
68
      if (priorityAndOrder.first < so.priorityAndOrder.first) {
×
69
        so.priorityAndOrder = priorityAndOrder;
×
70
      }
×
71
    });
×
72
  }
×
73
}
452✔
74

75
struct ZoneStatus
76
{
77
  bool isDnssecZone{false};
78
  bool isPresigned{false};
79
  bool isNSEC3{false};
80
  bool optOutFlag{false};
81
  NSEC3PARAMRecordContent ns3pr;
82

83
  bool isNarrow{false};
84
  unsigned int soa_serial{0};
85
  set<DNSName> nsset, qnames, secured;
86
  uint32_t domain_id;
87
  size_t numDeltas{0};
88
};
89

90
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
91
static bool catalogDiff(const DomainInfo& di, vector<CatalogInfo>& fromXFR, vector<CatalogInfo>& fromDB, const string& logPrefix)
92
{
12✔
93
  extern CommunicatorClass Communicator;
12✔
94

95
  bool doTransaction{true};
12✔
96
  bool inTransaction{false};
12✔
97
  CatalogInfo ciCreate, ciRemove;
12✔
98
  std::unordered_map<ZoneName, bool> clearCache;
12✔
99
  vector<CatalogInfo> retrieve;
12✔
100

101
  try {
12✔
102
    sort(fromXFR.begin(), fromXFR.end());
12✔
103
    sort(fromDB.begin(), fromDB.end());
12✔
104

105
    auto xfr = fromXFR.cbegin();
12✔
106
    auto db = fromDB.cbegin();
12✔
107

108
    while (xfr != fromXFR.end() || db != fromDB.end()) {
224!
109
      bool create{false};
212✔
110
      bool remove{false};
212✔
111

112
      if (xfr != fromXFR.end() && (db == fromDB.end() || *xfr < *db)) { // create
212!
113
        ciCreate = *xfr;
176✔
114
        create = true;
176✔
115
        ++xfr;
176✔
116
      }
176✔
117
      else if (db != fromDB.end() && (xfr == fromXFR.end() || *db < *xfr)) { // remove
36!
118
        ciRemove = *db;
12✔
119
        remove = true;
12✔
120
        ++db;
12✔
121
      }
12✔
122
      else {
24✔
123
        CatalogInfo ciXFR = *xfr;
24✔
124
        CatalogInfo ciDB = *db;
24✔
125
        if (ciDB.d_unique.empty() || ciXFR.d_unique == ciDB.d_unique) { // update
24!
126
          bool doOptions{false};
12✔
127

128
          if (ciDB.d_unique.empty()) { // set unique
12!
129
            g_log << Logger::Warning << logPrefix << "set unique, zone '" << ciXFR.d_zone << "' is now a member" << endl;
×
130
            ciDB.d_unique = ciXFR.d_unique;
×
131
            doOptions = true;
×
132
          }
×
133

134
          if (ciXFR.d_coo != ciDB.d_coo) { // update coo
12!
135
            g_log << Logger::Warning << logPrefix << "update coo for zone '" << ciXFR.d_zone << "' to '" << ciXFR.d_coo << "'" << endl;
×
136
            ciDB.d_coo = ciXFR.d_coo;
×
137
            doOptions = true;
×
138
          }
×
139

140
          if (ciXFR.d_group != ciDB.d_group) { // update group
12!
141
            g_log << Logger::Warning << logPrefix << "update group for zone '" << ciXFR.d_zone << "' to '" << boost::join(ciXFR.d_group, ", ") << "'" << endl;
12✔
142
            ciDB.d_group = ciXFR.d_group;
12✔
143
            doOptions = true;
12✔
144
          }
12✔
145

146
          if (doOptions) { // update zone options
12!
147
            if (doTransaction && (inTransaction = di.backend->startTransaction(di.zone))) {
12!
148
              g_log << Logger::Warning << logPrefix << "backend transaction started" << endl;
×
149
              doTransaction = false;
×
150
            }
×
151

152
            g_log << Logger::Warning << logPrefix << "update options for zone '" << ciXFR.d_zone << "'" << endl;
12✔
153
            di.backend->setOptions(ciXFR.d_zone, ciDB.toJson());
12✔
154
          }
12✔
155

156
          if (di.primaries != ciDB.d_primaries) { // update primaries
12!
157
            if (doTransaction && (inTransaction = di.backend->startTransaction(di.zone))) {
12!
158
              g_log << Logger::Warning << logPrefix << "backend transaction started" << endl;
×
159
              doTransaction = false;
×
160
            }
×
161

162
            vector<string> primaries;
12✔
163
            for (const auto& primary : di.primaries) {
12✔
164
              primaries.push_back(primary.toStringWithPortExcept(53));
12✔
165
            }
12✔
166
            g_log << Logger::Warning << logPrefix << "update primaries for zone '" << ciXFR.d_zone << "' to '" << boost::join(primaries, ", ") << "'" << endl;
12✔
167
            di.backend->setPrimaries(ciXFR.d_zone, di.primaries);
12✔
168

169
            retrieve.emplace_back(ciXFR);
12✔
170
          }
12✔
171
        }
12✔
172
        else { // reset
12✔
173
          ciCreate = *xfr;
12✔
174
          ciRemove = *db;
12✔
175
          create = true;
12✔
176
          remove = true;
12✔
177
        }
12✔
178
        ++xfr;
24✔
179
        ++db;
24✔
180
      }
24✔
181

182
      DomainInfo d;
212✔
183
      if (create && remove) {
212✔
184
        g_log << Logger::Warning << logPrefix << "zone '" << ciCreate.d_zone << "' state reset" << endl;
12✔
185
      }
12✔
186
      else if (create && di.backend->getDomainInfo(ciCreate.d_zone, d)) { // detect clash
200✔
187
        CatalogInfo ci;
36✔
188
        ci.fromJson(d.options, CatalogInfo::CatalogType::Consumer);
36✔
189

190
        if (di.zone != d.catalog && di.zone.operator const DNSName&() == ci.d_coo) {
36!
191
          if (ciCreate.d_unique == ci.d_unique) {
24✔
192
            g_log << Logger::Warning << logPrefix << "zone '" << d.zone << "' owner change without state reset, old catalog '" << d.catalog << "', new catalog '" << di.zone << "'" << endl;
12✔
193

194
            if (doTransaction && (inTransaction = di.backend->startTransaction(di.zone))) {
12!
195
              g_log << Logger::Warning << logPrefix << "backend transaction started" << endl;
×
196
              doTransaction = false;
×
197
            }
×
198

199
            di.backend->setPrimaries(ciCreate.d_zone, di.primaries);
12✔
200
            di.backend->setOptions(ciCreate.d_zone, ciCreate.toJson());
12✔
201
            di.backend->setCatalog(ciCreate.d_zone, di.zone);
12✔
202

203
            retrieve.emplace_back(ciCreate);
12✔
204
            continue;
12✔
205
          }
12✔
206
          g_log << Logger::Warning << logPrefix << "zone '" << d.zone << "' owner change with state reset, old catalog '" << d.catalog << "', new catalog '" << di.zone << "'" << endl;
12✔
207

208
          ciRemove.d_zone = d.zone;
12✔
209
          remove = true;
12✔
210
        }
12✔
211
        else {
12✔
212
          g_log << Logger::Warning << logPrefix << "zone '" << d.zone << "' already exists";
12✔
213
          if (!d.catalog.empty()) {
12!
214
            g_log << " in catalog '" << d.catalog;
×
215
          }
×
216
          g_log << "', create skipped" << endl;
12✔
217
          continue;
12✔
218
        }
12✔
219
      }
36✔
220

221
      if (remove) { // delete zone
188✔
222
        if (doTransaction && (inTransaction = di.backend->startTransaction(di.zone))) {
36!
223
          g_log << Logger::Warning << logPrefix << "backend transaction started" << endl;
×
224
          doTransaction = false;
×
225
        }
×
226

227
        g_log << Logger::Warning << logPrefix << "delete zone '" << ciRemove.d_zone << "'" << endl;
36✔
228
        di.backend->deleteDomain(ciRemove.d_zone);
36✔
229

230
        if (!create) {
36✔
231
          clearCache[ciRemove.d_zone] = false;
12✔
232
        }
12✔
233
      }
36✔
234

235
      if (create) { // create zone
188✔
236
        if (doTransaction && (inTransaction = di.backend->startTransaction(di.zone))) {
164!
237
          g_log << Logger::Warning << logPrefix << "backend transaction started" << endl;
12✔
238
          doTransaction = false;
12✔
239
        }
12✔
240

241
        g_log << Logger::Warning << logPrefix << "create zone '" << ciCreate.d_zone << "'" << endl;
164✔
242
        di.backend->createDomain(ciCreate.d_zone, DomainInfo::Secondary, ciCreate.d_primaries, "");
164✔
243

244
        di.backend->setPrimaries(ciCreate.d_zone, di.primaries);
164✔
245
        di.backend->setOptions(ciCreate.d_zone, ciCreate.toJson());
164✔
246
        di.backend->setCatalog(ciCreate.d_zone, di.zone);
164✔
247

248
        clearCache[ciCreate.d_zone] = true;
164✔
249
        retrieve.emplace_back(ciCreate);
164✔
250
      }
164✔
251
    }
188✔
252

253
    if (inTransaction && di.backend->commitTransaction()) {
12!
254
      g_log << Logger::Warning << logPrefix << "backend transaction committed" << endl;
12✔
255
    }
12✔
256

257
    // Update zonecache and clear all caches
258
    DomainInfo d;
12✔
259
    for (const auto& zone : clearCache) {
176✔
260
      if (g_zoneCache.isEnabled()) {
176✔
261
        if (zone.second) {
56✔
262
          if (di.backend->getDomainInfo(zone.first, d)) {
52!
263
            g_zoneCache.add(zone.first, d.id);
52✔
264
          }
52✔
265
          else {
×
266
            g_log << Logger::Error << logPrefix << "new zone '" << zone.first << "' does not exists and was not inserted in the zone-cache" << endl;
×
267
          }
×
268
        }
52✔
269
        else {
4✔
270
          g_zoneCache.remove(zone.first);
4✔
271
        }
4✔
272
      }
56✔
273

274
      DNSSECKeeper::clearCaches(zone.first);
176✔
275
      purgeAuthCaches(zone.first.operator const DNSName&().toString() + "$");
176✔
276
    }
176✔
277

278
    // retrieve new and updated zones with new primaries
279
    auto primaries = di.primaries;
12✔
280
    if (!primaries.empty()) {
12!
281
      for (auto& ret : retrieve) {
188✔
282
        shuffle(primaries.begin(), primaries.end(), pdns::dns_random_engine());
188✔
283
        const auto& primary = primaries.front();
188✔
284
        Communicator.addSuckRequest(ret.d_zone, primary, SuckRequest::Notify);
188✔
285
      }
188✔
286
    }
12✔
287

288
    return true;
12✔
289
  }
12✔
290
  catch (DBException& re) {
12✔
291
    g_log << Logger::Error << logPrefix << "DBException " << re.reason << endl;
×
292
  }
×
293
  catch (PDNSException& pe) {
12✔
294
    g_log << Logger::Error << logPrefix << "PDNSException " << pe.reason << endl;
×
295
  }
×
296
  catch (std::exception& re) {
12✔
297
    g_log << Logger::Error << logPrefix << "std::exception " << re.what() << endl;
×
298
  }
×
299

300
  if (di.backend && inTransaction) {
×
301
    g_log << Logger::Info << logPrefix << "aborting possible open transaction" << endl;
×
302
    di.backend->abortTransaction();
×
303
  }
×
304

305
  return false;
×
306
}
12✔
307

308
static bool catalogProcess(const DomainInfo& di, vector<DNSResourceRecord>& rrs, string logPrefix)
309
{
12✔
310
  logPrefix += "Catalog-Zone ";
12✔
311

312
  vector<CatalogInfo> fromXFR, fromDB;
12✔
313
  std::unordered_set<ZoneName> dupcheck;
12✔
314

315
  // From XFR
316
  bool hasSOA{false};
12✔
317
  bool zoneInvalid{false};
12✔
318
  int hasVersion{0};
12✔
319

320
  CatalogInfo ci;
12✔
321

322
  vector<DNSResourceRecord> ret;
12✔
323

324
  const auto compare = [](const DNSResourceRecord& a, const DNSResourceRecord& b) { return a.qname == b.qname ? a.qtype < b.qtype : a.qname.canonCompare(b.qname); };
1,404✔
325
  sort(rrs.begin(), rrs.end(), compare);
12✔
326

327
  DNSName rel;
12✔
328
  DNSName unique;
12✔
329
  for (auto& rr : rrs) {
272✔
330
    if (di.zone.operator const DNSName&() == rr.qname) {
272✔
331
      if (rr.qtype == QType::SOA) {
24✔
332
        hasSOA = true;
12✔
333
        continue;
12✔
334
      }
12✔
335
      if (rr.qtype == QType::NS) {
12!
336
        continue;
12✔
337
      }
12✔
338
    }
12✔
339

340
    else if (rr.qname == DNSName("version") + di.zone.operator const DNSName&() && rr.qtype == QType::TXT) {
248!
341
      if (hasVersion) {
12!
342
        g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "', multiple version records found, aborting" << endl;
×
343
        return false;
×
344
      }
×
345

346
      if (rr.content == "\"1\"") {
12!
347
        hasVersion = 1;
×
348
      }
×
349
      else if (rr.content == "\"2\"") {
12!
350
        hasVersion = 2;
12✔
351
      }
12✔
352
      else {
×
353
        g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "', unsupported catalog zone schema version " << rr.content << ", aborting" << endl;
×
354
        return false;
×
355
      }
×
356
    }
12✔
357

358
    else if (rr.qname.isPartOf(DNSName("zones") + di.zone.operator const DNSName&())) {
236!
359
      if (rel.empty() && !hasVersion) {
236!
360
        g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "', catalog zone schema version missing, aborting" << endl;
×
361
        return false;
×
362
      }
×
363

364
      rel = rr.qname.makeRelative(DNSName("zones") + di.zone.operator const DNSName&());
236✔
365

366
      if (rel.countLabels() == 1 && rr.qtype == QType::PTR) {
236!
367
        if (!unique.empty()) {
200✔
368
          if (rel != unique) {
188!
369
            fromXFR.emplace_back(ci);
188✔
370
          }
188✔
371
          else {
×
372
            g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "', duplicate unique '" << unique << "'" << endl;
×
373
            zoneInvalid = true;
×
374
          }
×
375
        }
188✔
376

377
        unique = rel;
200✔
378

379
        ci = {};
200✔
380
        ci.setType(CatalogInfo::CatalogType::Consumer);
200✔
381
        ci.d_zone = ZoneName(rr.content);
200✔
382
        ci.d_unique = unique;
200✔
383

384
        if (!dupcheck.insert(ci.d_zone).second) {
200!
385
          g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "', duplicate member zone'" << ci.d_zone << "'" << endl;
×
386
          zoneInvalid = true;
×
387
        }
×
388
      }
200✔
389

390
      else if (hasVersion == 2) {
36!
391
        if (rel == (DNSName("coo") + unique) && rr.qtype == QType::PTR) {
36!
392
          if (!ci.d_coo.empty()) {
12!
393
            g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "', duplicate COO for unique '" << unique << "'" << endl;
×
394
            zoneInvalid = true;
×
395
          }
×
396
          else {
12✔
397
            ci.d_coo = DNSName(rr.content);
12✔
398
          }
12✔
399
        }
12✔
400
        else if (rel == (DNSName("group") + unique) && rr.qtype == QType::TXT) {
24!
401
          std::string content = rr.content;
24✔
402
          if (content.length() >= 2 && content.at(0) == '\"' && content.at(content.length() - 1) == '\"') { // TXT pain
24!
403
            content = content.substr(1, content.length() - 2);
24✔
404
          }
24✔
405
          ci.d_group.insert(content);
24✔
406
        }
24✔
407
      }
36✔
408
    }
236✔
409
    rr.disabled = true;
248✔
410
  }
248✔
411
  if (!ci.d_zone.empty()) {
12!
412
    fromXFR.emplace_back(ci);
12✔
413
  }
12✔
414

415
  if (!hasSOA || !hasVersion || zoneInvalid) {
12!
416
    g_log << Logger::Warning << logPrefix << "zone '" << di.zone << "' is invalid, skip updates" << endl;
×
417
    return false;
×
418
  }
×
419

420
  // Get catalog ifo from db
421
  if (!di.backend->getCatalogMembers(di.zone, fromDB, CatalogInfo::CatalogType::Consumer)) {
12!
422
    return false;
×
423
  }
×
424

425
  // Process
426
  return catalogDiff(di, fromXFR, fromDB, logPrefix);
12✔
427
}
12✔
428

429
void CommunicatorClass::ixfrSuck(const ZoneName& domain, const TSIGTriplet& tsig, const ComboAddress& laddr, const ComboAddress& remote, ZoneStatus& status, vector<DNSRecord>* axfr)
430
{
×
431
  string logPrefix = "IXFR-in zone '" + domain.toLogString() + "', primary '" + remote.toString() + "', ";
×
432

433
  UeberBackend B; // fresh UeberBackend
×
434

435
  DomainInfo di;
×
436
  di.backend = nullptr;
×
437
  //  bool transaction=false;
438
  try {
×
439
    DNSSECKeeper dk(&B); // reuse our UeberBackend copy for DNSSECKeeper
×
440

441
    bool wrongDomainKind = false;
×
442
    // this checks three error conditions, and sets wrongDomainKind if we hit the third & had an error
443
    if (!B.getDomainInfo(domain, di) || !di.backend || (wrongDomainKind = true, di.kind != DomainInfo::Secondary)) { // di.backend and B are mostly identical
×
444
      if (wrongDomainKind)
×
445
        g_log << Logger::Warning << logPrefix << "can't determine backend, not configured as secondary" << endl;
×
446
      else
×
447
        g_log << Logger::Warning << logPrefix << "can't determine backend" << endl;
×
448
      return;
×
449
    }
×
450

451
    uint16_t xfrTimeout = ::arg().asNum("axfr-fetch-timeout");
×
452
    soatimes drsoa_soatimes = {di.serial, 0, 0, 0, 0};
×
453
    DNSRecord drsoa;
×
454
    drsoa.setContent(std::make_shared<SOARecordContent>(g_rootdnsname, g_rootdnsname, drsoa_soatimes));
×
455
    auto deltas = getIXFRDeltas(remote, domain.operator const DNSName&(), drsoa, xfrTimeout, false, tsig, laddr.sin4.sin_family != 0 ? &laddr : nullptr, ((size_t)::arg().asNum("xfr-max-received-mbytes")) * 1024 * 1024);
×
456
    status.numDeltas = deltas.size();
×
457
    //    cout<<"Got "<<deltas.size()<<" deltas from serial "<<di.serial<<", applying.."<<endl;
458

459
    for (const auto& d : deltas) {
×
460
      const auto& remove = d.first;
×
461
      const auto& add = d.second;
×
462
      //      cout<<"Delta sizes: "<<remove.size()<<", "<<add.size()<<endl;
463

464
      if (remove.empty()) { // we got passed an AXFR!
×
465
        *axfr = add;
×
466
        return;
×
467
      }
×
468

469
      // our hammer is 'replaceRRSet(domain_id, qname, qt, vector<DNSResourceRecord>& rrset)
470
      // which thinks in terms of RRSETs
471
      // however, IXFR does not, and removes and adds *records* (bummer)
472
      // this means that we must group updates by {qname,qtype}, retrieve the RRSET, apply
473
      // the add/remove updates, and replaceRRSet the whole thing.
474

475
      map<pair<ZoneName, uint16_t>, pair<vector<DNSRecord>, vector<DNSRecord>>> grouped;
×
476

477
      for (const auto& x : remove)
×
478
        grouped[{ZoneName(x.d_name), x.d_type}].first.push_back(x);
×
479
      for (const auto& x : add)
×
480
        grouped[{ZoneName(x.d_name), x.d_type}].second.push_back(x);
×
481

482
      di.backend->startTransaction(domain, UnknownDomainID);
×
483
      for (const auto& g : grouped) {
×
484
        vector<DNSRecord> rrset;
×
485
        {
×
486
          DNSZoneRecord zrr;
×
487
          di.backend->lookup(QType(g.first.second), g.first.first.operator const DNSName&() + domain.operator const DNSName&(), di.id);
×
488
          while (di.backend->get(zrr)) {
×
489
            zrr.dr.d_name.makeUsRelative(domain);
×
490
            rrset.push_back(zrr.dr);
×
491
          }
×
492
        }
×
493
        // O(N^2)!
494
        rrset.erase(remove_if(rrset.begin(), rrset.end(),
×
495
                              [&g](const DNSRecord& dr) {
×
496
                                return count(g.second.first.cbegin(),
×
497
                                             g.second.first.cend(), dr);
×
498
                              }),
×
499
                    rrset.end());
×
500
        // the DNSRecord== operator compares on name, type, class and lowercase content representation
501

502
        for (const auto& x : g.second.second) {
×
503
          rrset.push_back(x);
×
504
        }
×
505

506
        vector<DNSResourceRecord> replacement;
×
507
        for (const auto& dr : rrset) {
×
508
          auto rr = DNSResourceRecord::fromWire(dr);
×
509
          rr.qname += domain.operator const DNSName&();
×
510
          rr.domain_id = di.id;
×
511
          if (dr.d_type == QType::SOA) {
×
512
            //            cout<<"New SOA: "<<x.d_content->getZoneRepresentation()<<endl;
513
            auto sr = getRR<SOARecordContent>(dr);
×
514
            status.soa_serial = sr->d_st.serial;
×
515
          }
×
516

517
          replacement.emplace_back(std::move(rr));
×
518
        }
×
519

520
        di.backend->replaceRRSet(di.id, g.first.first.operator const DNSName&() + domain.operator const DNSName&(), QType(g.first.second), replacement);
×
521
      }
×
522
      di.backend->commitTransaction();
×
523
    }
×
524
  }
×
525
  catch (std::exception& p) {
×
526
    g_log << Logger::Error << logPrefix << "got exception (std::exception): " << p.what() << endl;
×
527
    throw;
×
528
  }
×
529
  catch (PDNSException& p) {
×
530
    g_log << Logger::Error << logPrefix << "got exception (PDNSException): " << p.reason << endl;
×
531
    throw;
×
532
  }
×
533
}
×
534

535
static bool processRecordForZS(const DNSName& domain, bool& firstNSEC3, DNSResourceRecord& rr, ZoneStatus& zs)
536
{
1,501,863✔
537
  switch (rr.qtype.getCode()) {
1,501,863✔
538
  case QType::NSEC3PARAM:
214✔
539
    zs.ns3pr = NSEC3PARAMRecordContent(rr.content);
214✔
540
    zs.isDnssecZone = zs.isNSEC3 = true;
214✔
541
    zs.isNarrow = false;
214✔
542
    return false;
214✔
543
  case QType::NSEC3: {
242,552✔
544
    NSEC3RecordContent ns3rc(rr.content);
242,552✔
545
    if (firstNSEC3) {
242,552✔
546
      zs.isDnssecZone = zs.isPresigned = true;
214✔
547
      firstNSEC3 = false;
214✔
548
    }
214✔
549
    else if (zs.optOutFlag != (ns3rc.d_flags & 1))
242,338!
550
      throw PDNSException("Zones with a mixture of Opt-Out NSEC3 RRs and non-Opt-Out NSEC3 RRs are not supported.");
×
551
    zs.optOutFlag = ns3rc.d_flags & 1;
242,552✔
552
    if (ns3rc.isSet(QType::NS) && !(rr.qname == domain)) {
242,552!
553
      DNSName hashPart = rr.qname.makeRelative(domain);
334✔
554
      zs.secured.insert(hashPart);
334✔
555
    }
334✔
556
    return false;
242,552✔
557
  }
242,552✔
558

559
  case QType::NSEC:
100,942✔
560
    zs.isDnssecZone = zs.isPresigned = true;
100,942✔
561
    return false;
100,942✔
562

563
  case QType::NS:
1,350✔
564
    if (rr.qname != domain)
1,350✔
565
      zs.nsset.insert(rr.qname);
530✔
566
    break;
1,350✔
567
  }
1,501,863✔
568

569
  zs.qnames.insert(rr.qname);
1,158,154✔
570

571
  rr.domain_id = zs.domain_id;
1,158,154✔
572
  return true;
1,158,154✔
573
}
1,501,863✔
574

575
/* So this code does a number of things.
576
   1) It will AXFR a domain from a primary
577
      The code can retrieve the current serial number in the database itself.
578
      It may attempt an IXFR
579
   2) It will filter the zone through a lua *filter* script
580
   3) The code walks through the zone records do determine DNSSEC status (secured, nsec/nsec3, optout)
581
   4) It inserts the zone into the database
582
      With the right 'ordername' fields
583
   5) It updates the Empty Non Terminals
584
*/
585

586
static vector<DNSResourceRecord> doAxfr(const ComboAddress& raddr, const DNSName& domain, const TSIGTriplet& tt, const ComboAddress& laddr, unique_ptr<AuthLua4>& pdl, ZoneStatus& zs)
587
{
448✔
588
  uint16_t axfr_timeout = ::arg().asNum("axfr-fetch-timeout");
448✔
589
  vector<DNSResourceRecord> rrs;
448✔
590
  AXFRRetriever retriever(raddr, ZoneName(domain), tt, (laddr.sin4.sin_family == 0) ? nullptr : &laddr, ((size_t)::arg().asNum("xfr-max-received-mbytes")) * 1024 * 1024, axfr_timeout);
448!
591
  Resolver::res_t recs;
448✔
592
  bool first = true;
448✔
593
  bool firstNSEC3{true};
448✔
594
  bool soa_received{false};
448✔
595
  string logPrefix = "AXFR-in zone '" + domain.toLogString() + "', primary '" + raddr.toString() + "', ";
448✔
596
  while (retriever.getChunk(recs, nullptr, axfr_timeout)) {
20,035✔
597
    if (first) {
19,587✔
598
      g_log << Logger::Notice << logPrefix << "retrieval started" << endl;
448✔
599
      first = false;
448✔
600
    }
448✔
601

602
    for (auto& rec : recs) {
1,501,931✔
603
      rec.qname.makeUsLowerCase();
1,501,931✔
604
      if (rec.qtype.getCode() == QType::OPT || rec.qtype.getCode() == QType::TSIG) // ignore EDNS0 & TSIG
1,501,932✔
605
        continue;
69✔
606

607
      if (!rec.qname.isPartOf(domain)) {
1,501,862!
608
        g_log << Logger::Warning << logPrefix << "primary tried to sneak in out-of-zone data '" << rec.qname << "'|" << rec.qtype.toString() << ", ignoring" << endl;
×
609
        continue;
×
610
      }
×
611

612
      vector<DNSResourceRecord> out;
1,501,862✔
613
      if (!pdl || !pdl->axfrfilter(raddr, domain, rec, out)) {
1,501,862!
614
        out.push_back(rec); // if axfrfilter didn't do anything, we put our record in 'out' ourselves
1,501,862✔
615
      }
1,501,862✔
616

617
      for (auto& rr : out) {
1,501,863✔
618
        if (!rr.qname.isPartOf(domain)) {
1,501,863!
619
          g_log << Logger::Error << logPrefix << "axfrfilter() filter tried to sneak in out-of-zone data '" << rr.qname << "'|" << rr.qtype.toString() << ", ignoring" << endl;
×
620
          continue;
×
621
        }
×
622
        if (!processRecordForZS(domain, firstNSEC3, rr, zs))
1,501,863✔
623
          continue;
343,708✔
624
        if (rr.qtype.getCode() == QType::SOA) {
1,158,155✔
625
          if (soa_received)
895✔
626
            continue; // skip the last SOA
448✔
627
          SOAData sd;
447✔
628
          fillSOAData(rr.content, sd);
447✔
629
          zs.soa_serial = sd.serial;
447✔
630
          soa_received = true;
447✔
631
        }
447✔
632

633
        rrs.push_back(rr);
1,157,707✔
634
      }
1,157,707✔
635
    }
1,501,862✔
636
  }
19,587✔
637
  return rrs;
448✔
638
}
448✔
639

640
void CommunicatorClass::suck(const ZoneName& domain, const ComboAddress& remote, bool force) // NOLINT(readability-function-cognitive-complexity)
641
{
448✔
642
  {
448✔
643
    auto data = d_data.lock();
448✔
644
    if (data->d_inprogress.count(domain)) {
448!
645
      return;
×
646
    }
×
647
    data->d_inprogress.insert(domain);
448✔
648
  }
448✔
649
  RemoveSentinel rs(domain, this); // this removes us from d_inprogress when we go out of scope
×
650

651
  string logPrefix = "XFR-in zone: '" + domain.toLogString() + "', primary: '" + remote.toString() + "', ";
448✔
652

653
  g_log << Logger::Notice << logPrefix << "initiating transfer" << endl;
448✔
654
  UeberBackend B; // fresh UeberBackend
448✔
655

656
  DomainInfo di;
448✔
657
  di.backend = nullptr;
448✔
658
  bool transaction = false;
448✔
659
  try {
448✔
660
    DNSSECKeeper dk(&B); // reuse our UeberBackend copy for DNSSECKeeper
448✔
661
    bool wrongDomainKind = false;
448✔
662
    // this checks three error conditions & sets wrongDomainKind if we hit the third
663
    if (!B.getDomainInfo(domain, di) || !di.backend || (wrongDomainKind = true, !force && !di.isSecondaryType())) { // di.backend and B are mostly identical
448!
664
      if (wrongDomainKind)
×
665
        g_log << Logger::Warning << logPrefix << "can't determine backend, not configured as secondary" << endl;
×
666
      else
×
667
        g_log << Logger::Warning << logPrefix << "can't determine backend" << endl;
×
668
      return;
×
669
    }
×
670
    ZoneStatus zs;
448✔
671
    zs.domain_id = di.id;
448✔
672

673
    TSIGTriplet tt;
448✔
674
    if (dk.getTSIGForAccess(domain, remote, &tt.name)) {
448✔
675
      string tsigsecret64;
23✔
676
      if (B.getTSIGKey(tt.name, tt.algo, tsigsecret64)) {
23!
677
        if (B64Decode(tsigsecret64, tt.secret)) {
23!
678
          g_log << Logger::Error << logPrefix << "unable to Base-64 decode TSIG key '" << tt.name << "' or zone not found" << endl;
×
679
          return;
×
680
        }
×
681
      }
23✔
682
      else {
×
683
        g_log << Logger::Warning << logPrefix << "TSIG key '" << tt.name << "' for zone not found" << endl;
×
684
        return;
×
685
      }
×
686
    }
23✔
687

688
    unique_ptr<AuthLua4> pdl{nullptr};
448✔
689
    vector<string> scripts;
448✔
690
    string script = ::arg()["lua-axfr-script"];
448✔
691
    if (B.getDomainMetadata(domain, "LUA-AXFR-SCRIPT", scripts) && !scripts.empty()) {
448!
692
      if (pdns_iequals(scripts[0], "NONE")) {
×
693
        script.clear();
×
694
      }
×
695
      else {
×
696
        script = scripts[0];
×
697
      }
×
698
    }
×
699
    if (!script.empty()) {
448!
700
      try {
×
701
        pdl = make_unique<AuthLua4>(::arg()["lua-global-include-dir"]);
×
702
        pdl->loadFile(script);
×
703
        g_log << Logger::Info << logPrefix << "loaded Lua script '" << script << "'" << endl;
×
704
      }
×
705
      catch (std::exception& e) {
×
706
        g_log << Logger::Error << logPrefix << "failed to load Lua script '" << script << "': " << e.what() << endl;
×
707
        return;
×
708
      }
×
709
    }
×
710

711
    vector<string> localaddr;
448✔
712
    ComboAddress laddr;
448✔
713

714
    if (B.getDomainMetadata(domain, "AXFR-SOURCE", localaddr) && !localaddr.empty()) {
448!
715
      try {
22✔
716
        laddr = ComboAddress(localaddr[0]);
22✔
717
        g_log << Logger::Info << logPrefix << "xfr source set to " << localaddr[0] << endl;
22✔
718
      }
22✔
719
      catch (std::exception& e) {
22✔
720
        g_log << Logger::Error << logPrefix << "failed to set xfr source '" << localaddr[0] << "': " << e.what() << endl;
×
721
        return;
×
722
      }
×
723
    }
22✔
724
    else {
426✔
725
      if (!pdns::isQueryLocalAddressFamilyEnabled(remote.sin4.sin_family)) {
426!
726
        bool isV6 = remote.sin4.sin_family == AF_INET6;
×
727
        g_log << Logger::Warning << logPrefix << "unable to xfr, address family (IPv" << (isV6 ? "6" : "4") << " is not enabled for outgoing traffic (query-local-address)" << endl;
×
728
        return;
×
729
      }
×
730
      laddr = pdns::getQueryLocalAddress(remote.sin4.sin_family, 0);
426✔
731
    }
426✔
732

733
    bool hadDnssecZone = false;
448✔
734
    bool hadPresigned = false;
448✔
735
    bool hadNSEC3 = false;
448✔
736
    NSEC3PARAMRecordContent hadNs3pr;
448✔
737
    bool hadNarrow = false;
448✔
738

739
    vector<DNSResourceRecord> rrs;
448✔
740
    if (dk.isSecuredZone(domain, false)) {
448✔
741
      hadDnssecZone = true;
18✔
742
      hadPresigned = dk.isPresigned(domain, false);
18✔
743
      if (dk.getNSEC3PARAM(domain, &zs.ns3pr, &zs.isNarrow, false)) {
18✔
744
        hadNSEC3 = true;
12✔
745
        hadNs3pr = zs.ns3pr;
12✔
746
        hadNarrow = zs.isNarrow;
12✔
747
      }
12✔
748
    }
18✔
749
    else if (di.serial) {
430✔
750
      vector<string> meta;
6✔
751
      B.getDomainMetadata(domain, "IXFR", meta);
6✔
752
      if (!meta.empty() && meta[0] == "1") {
6!
753
        logPrefix = "I" + logPrefix; // XFR -> IXFR
×
754
        vector<DNSRecord> axfr;
×
755
        g_log << Logger::Notice << logPrefix << "starting IXFR" << endl;
×
756
        CommunicatorClass::ixfrSuck(domain, tt, laddr, remote, zs, &axfr);
×
757
        if (!axfr.empty()) {
×
758
          g_log << Logger::Notice << logPrefix << "IXFR turned into an AXFR" << endl;
×
759
          logPrefix[0] = 'A'; // IXFR -> AXFR
×
760
          bool firstNSEC3 = true;
×
761
          rrs.reserve(axfr.size());
×
762
          for (const auto& dr : axfr) {
×
763
            auto rr = DNSResourceRecord::fromWire(dr);
×
764
            rr.qname += domain.operator const DNSName&();
×
765
            rr.qname.makeUsLowerCase();
×
766
            rr.domain_id = zs.domain_id;
×
767
            if (!processRecordForZS(domain.operator const DNSName&(), firstNSEC3, rr, zs)) {
×
768
              continue;
×
769
            }
×
770
            if (dr.d_type == QType::SOA) {
×
771
              auto sd = getRR<SOARecordContent>(dr);
×
772
              zs.soa_serial = sd->d_st.serial;
×
773
            }
×
774
            rrs.emplace_back(std::move(rr));
×
775
          }
×
776
        }
×
777
        else {
×
778
          g_log << Logger::Warning << logPrefix << "got " << zs.numDeltas << " delta" << addS(zs.numDeltas) << ", zone committed with serial " << zs.soa_serial << endl;
×
NEW
779
          purgeAuthCaches(domain.operator const DNSName&().toString() + "$");
×
780
          return;
×
781
        }
×
782
      }
×
783
    }
6✔
784

785
    if (rrs.empty()) {
448!
786
      g_log << Logger::Notice << logPrefix << "starting AXFR" << endl;
448✔
787
      rrs = doAxfr(remote, domain.operator const DNSName&(), tt, laddr, pdl, zs);
448✔
788
      logPrefix = "A" + logPrefix; // XFR -> AXFR
448✔
789
      g_log << Logger::Notice << logPrefix << "retrieval finished" << endl;
448✔
790
    }
448✔
791

792
    if (di.kind == DomainInfo::Consumer) {
448✔
793
      if (!catalogProcess(di, rrs, logPrefix)) {
12!
794
        g_log << Logger::Warning << logPrefix << "Catalog-Zone update failed, only import records" << endl;
×
795
      }
×
796
    }
12✔
797

798
    if (zs.isNSEC3) {
448✔
799
      zs.ns3pr.d_flags = zs.optOutFlag ? 1 : 0;
214✔
800
    }
214✔
801

802
    if (!zs.isPresigned) {
448✔
803
      DNSSECKeeper::keyset_t keys = dk.getKeys(domain, false);
144✔
804
      if (!keys.empty()) {
144!
805
        zs.isDnssecZone = true;
×
806
        zs.isNSEC3 = hadNSEC3;
×
807
        zs.ns3pr = hadNs3pr;
×
808
        zs.optOutFlag = (hadNs3pr.d_flags & 1);
×
809
        zs.isNarrow = hadNarrow;
×
810
      }
×
811
    }
144✔
812

813
    if (zs.isDnssecZone) {
448✔
814
      if (!zs.isNSEC3)
304✔
815
        g_log << Logger::Debug << logPrefix << "adding NSEC ordering information" << endl;
90✔
816
      else if (!zs.isNarrow)
214!
817
        g_log << Logger::Debug << logPrefix << "adding NSEC3 hashed ordering information" << endl;
214✔
818
      else
×
819
        g_log << Logger::Debug << logPrefix << "zone is narrow, only setting 'auth' fields" << endl;
×
820
    }
304✔
821

822
    transaction = di.backend->startTransaction(domain, zs.domain_id);
448✔
823
    g_log << Logger::Info << logPrefix << "storage transaction started" << endl;
448✔
824

825
    // update the presigned flag and NSEC3PARAM
826
    if (zs.isDnssecZone) {
448✔
827
      // update presigned if there was a change
828
      if (zs.isPresigned && !hadPresigned) {
304!
829
        // zone is now presigned
830
        dk.setPresigned(domain);
286✔
831
      }
286✔
832
      else if (hadPresigned && !zs.isPresigned) {
18!
833
        // zone is no longer presigned
834
        dk.unsetPresigned(domain);
×
835
      }
×
836
      // update NSEC3PARAM
837
      if (zs.isNSEC3) {
304✔
838
        // zone is NSEC3, only update if there was a change
839
        if (!hadNSEC3 || (hadNarrow != zs.isNarrow) || (zs.ns3pr.d_algorithm != hadNs3pr.d_algorithm) || (zs.ns3pr.d_flags != hadNs3pr.d_flags) || (zs.ns3pr.d_iterations != hadNs3pr.d_iterations) || (zs.ns3pr.d_salt != hadNs3pr.d_salt)) {
214!
840
          dk.setNSEC3PARAM(domain, zs.ns3pr, zs.isNarrow);
202✔
841
        }
202✔
842
      }
214✔
843
      else if (hadNSEC3) {
90!
844
        // zone is no longer NSEC3
845
        dk.unsetNSEC3PARAM(domain);
×
846
      }
×
847
    }
304✔
848
    else if (hadDnssecZone) {
144!
849
      // zone is no longer signed
850
      if (hadPresigned) {
×
851
        // remove presigned
852
        dk.unsetPresigned(domain);
×
853
      }
×
854
      if (hadNSEC3) {
×
855
        // unset NSEC3PARAM
856
        dk.unsetNSEC3PARAM(domain);
×
857
      }
×
858
    }
×
859

860
    bool doent = true;
448✔
861
    uint32_t maxent = ::arg().asNum("max-ent-entries");
448✔
862
    DNSName shorter, ordername;
448✔
863
    set<DNSName> rrterm;
448✔
864
    map<DNSName, bool> nonterm;
448✔
865

866
    for (DNSResourceRecord& rr : rrs) {
1,157,705✔
867
      if (!zs.isPresigned) {
1,157,705✔
868
        if (rr.qtype.getCode() == QType::RRSIG)
122,604!
869
          continue;
×
870
        if (zs.isDnssecZone && rr.qtype.getCode() == QType::DNSKEY && !::arg().mustDo("direct-dnskey"))
122,604!
871
          continue;
×
872
      }
122,604✔
873

874
      // Figure out auth and ents
875
      rr.auth = true;
1,157,705✔
876
      shorter = rr.qname;
1,157,705✔
877
      rrterm.clear();
1,157,705✔
878
      do {
2,315,371✔
879
        if (doent) {
2,315,371✔
880
          if (!zs.qnames.count(shorter))
2,315,366✔
881
            rrterm.insert(shorter);
3,370✔
882
        }
2,315,366✔
883
        if (zs.nsset.count(shorter) && rr.qtype.getCode() != QType::DS)
2,315,371✔
884
          rr.auth = false;
1,064✔
885

886
        if (shorter == domain.operator const DNSName&()) { // stop at apex
2,315,371✔
887
          break;
1,157,707✔
888
        }
1,157,707✔
889
      } while (shorter.chopOff());
2,315,371✔
890

891
      // Insert ents
892
      if (doent && !rrterm.empty()) {
1,157,707✔
893
        bool auth;
2,659✔
894
        if (!rr.auth && rr.qtype.getCode() == QType::NS) {
2,659✔
895
          if (zs.isNSEC3)
58✔
896
            ordername = DNSName(toBase32Hex(hashQNameWithSalt(zs.ns3pr, rr.qname)));
30✔
897
          auth = (!zs.isNSEC3 || !zs.optOutFlag || zs.secured.count(ordername));
58!
898
        }
58✔
899
        else
2,601✔
900
          auth = rr.auth;
2,601✔
901

902
        for (const auto& nt : rrterm) {
3,370✔
903
          if (!nonterm.count(nt))
3,370✔
904
            nonterm.insert(pair<DNSName, bool>(nt, auth));
938✔
905
          else if (auth)
2,432✔
906
            nonterm[nt] = true;
2,336✔
907
        }
3,370✔
908

909
        if (nonterm.size() > maxent) {
2,659!
910
          g_log << Logger::Warning << logPrefix << "zone has too many empty non terminals" << endl;
×
911
          nonterm.clear();
×
912
          doent = false;
×
913
        }
×
914
      }
2,659✔
915

916
      // RRSIG is always auth, even inside a delegation
917
      if (rr.qtype.getCode() == QType::RRSIG)
1,157,705✔
918
        rr.auth = true;
688,338✔
919

920
      // Add ordername and insert record
921
      if (zs.isDnssecZone && rr.qtype.getCode() != QType::RRSIG) {
1,157,705✔
922
        if (zs.isNSEC3) {
346,765✔
923
          // NSEC3
924
          ordername = DNSName(toBase32Hex(hashQNameWithSalt(zs.ns3pr, rr.qname)));
244,756✔
925
          if (!zs.isNarrow && (rr.auth || (rr.qtype.getCode() == QType::NS && (!zs.optOutFlag || zs.secured.count(ordername))))) {
244,756!
926
            di.backend->feedRecord(rr, ordername, true);
244,450✔
927
          }
244,450✔
928
          else
306✔
929
            di.backend->feedRecord(rr, DNSName());
306✔
930
        }
244,756✔
931
        else {
102,009✔
932
          // NSEC
933
          if (rr.auth || rr.qtype.getCode() == QType::NS) {
102,009✔
934
            ordername = rr.qname.makeRelative(domain);
101,926✔
935
            di.backend->feedRecord(rr, ordername);
101,926✔
936
          }
101,926✔
937
          else
83✔
938
            di.backend->feedRecord(rr, DNSName());
83✔
939
        }
102,009✔
940
      }
346,765✔
941
      else
810,940✔
942
        di.backend->feedRecord(rr, DNSName());
810,940✔
943
    }
1,157,705✔
944

945
    // Insert empty non-terminals
946
    if (doent && !nonterm.empty()) {
448!
947
      if (zs.isNSEC3) {
197✔
948
        di.backend->feedEnts3(zs.domain_id, domain.operator const DNSName&(), nonterm, zs.ns3pr, zs.isNarrow);
96✔
949
      }
96✔
950
      else
101✔
951
        di.backend->feedEnts(zs.domain_id, nonterm);
101✔
952
    }
197✔
953

954
    di.backend->commitTransaction();
448✔
955
    transaction = false;
448✔
956
    di.backend->setFresh(zs.domain_id);
448✔
957
    purgeAuthCaches(domain.operator const DNSName&().toString() + "$");
448✔
958

959
    g_log << Logger::Warning << logPrefix << "zone committed with serial " << zs.soa_serial << endl;
448✔
960

961
    // Send secondary re-notifications
962
    bool doNotify;
448✔
963
    vector<string> meta;
448✔
964
    if (B.getDomainMetadata(domain, "SLAVE-RENOTIFY", meta) && !meta.empty()) {
448!
965
      doNotify = (meta.front() == "1");
×
966
    }
×
967
    else {
448✔
968
      doNotify = (::arg().mustDo("secondary-do-renotify"));
448✔
969
    }
448✔
970
    if (doNotify) {
448!
971
      notifyDomain(domain, &B);
×
972
    }
×
973
  }
448✔
974
  catch (DBException& re) {
448✔
975
    g_log << Logger::Error << logPrefix << "unable to feed record: " << re.reason << endl;
×
976
    if (di.backend && transaction) {
×
977
      g_log << Logger::Info << logPrefix << "aborting possible open transaction" << endl;
×
978
      di.backend->abortTransaction();
×
979
    }
×
980
  }
×
981
  catch (const MOADNSException& mde) {
448✔
982
    g_log << Logger::Error << logPrefix << "unable to parse record (MOADNSException): " << mde.what() << endl;
×
983
    if (di.backend && transaction) {
×
984
      g_log << Logger::Info << logPrefix << "aborting possible open transaction" << endl;
×
985
      di.backend->abortTransaction();
×
986
    }
×
987
  }
×
988
  catch (std::exception& re) {
448✔
989
    g_log << Logger::Error << logPrefix << "unable to xfr zone (std::exception): " << re.what() << endl;
×
990
    if (di.backend && transaction) {
×
991
      g_log << Logger::Info << logPrefix << "aborting possible open transaction" << endl;
×
992
      di.backend->abortTransaction();
×
993
    }
×
994
  }
×
995
  catch (ResolverException& re) {
448✔
996
    {
×
997
      auto data = d_data.lock();
×
998
      // The AXFR probably failed due to a problem on the primary server. If SOA-checks against this primary
999
      // still succeed, we would constantly try to AXFR the zone. To avoid this, we add the zone to the list of
1000
      // failed secondary-checks. This will suspend secondary-checks (and subsequent AXFR) for this zone for some time.
1001
      uint64_t newCount = 1;
×
1002
      time_t now = time(nullptr);
×
1003
      const auto failedEntry = data->d_failedSecondaryRefresh.find(domain);
×
1004
      if (failedEntry != data->d_failedSecondaryRefresh.end()) {
×
1005
        newCount = data->d_failedSecondaryRefresh[domain].first + 1;
×
1006
      }
×
1007
      time_t nextCheck = now + std::min(newCount * d_tickinterval, (uint64_t)::arg().asNum("default-ttl"));
×
1008
      data->d_failedSecondaryRefresh[domain] = {newCount, nextCheck};
×
1009
      g_log << Logger::Warning << logPrefix << "unable to xfr zone (ResolverException): " << re.reason << " (This was attempt number " << newCount << ". Excluding zone from secondary-checks until " << nextCheck << ")" << endl;
×
1010
    }
×
1011
    if (di.backend && transaction) {
×
1012
      g_log << Logger::Info << "aborting possible open transaction" << endl;
×
1013
      di.backend->abortTransaction();
×
1014
    }
×
1015
  }
×
1016
  catch (PDNSException& ae) {
448✔
1017
    g_log << Logger::Error << logPrefix << "unable to xfr zone (PDNSException): " << ae.reason << endl;
×
1018
    if (di.backend && transaction) {
×
1019
      g_log << Logger::Info << logPrefix << "aborting possible open transaction" << endl;
×
1020
      di.backend->abortTransaction();
×
1021
    }
×
1022
  }
×
1023
}
448✔
1024
namespace
1025
{
1026
struct DomainNotificationInfo
1027
{
1028
  DomainInfo di;
1029
  bool dnssecOk;
1030
  ComboAddress localaddr;
1031
  DNSName tsigkeyname, tsigalgname;
1032
  string tsigsecret;
1033
};
1034
}
1035

1036
struct SecondarySenderReceiver
1037
{
1038
  typedef std::tuple<DNSName, ComboAddress, uint16_t> Identifier;
1039

1040
  struct Answer
1041
  {
1042
    uint32_t theirSerial;
1043
    uint32_t theirInception;
1044
    uint32_t theirExpire;
1045
  };
1046

1047
  map<uint32_t, Answer> d_freshness;
1048

1049
  void deliverTimeout(const Identifier& /* i */)
1050
  {
26✔
1051
  }
26✔
1052

1053
  Identifier send(DomainNotificationInfo& dni)
1054
  {
300✔
1055
    shuffle(dni.di.primaries.begin(), dni.di.primaries.end(), pdns::dns_random_engine());
300✔
1056
    try {
300✔
1057
      return {dni.di.zone.operator const DNSName&(),
300✔
1058
              *dni.di.primaries.begin(),
300✔
1059
              d_resolver.sendResolve(*dni.di.primaries.begin(),
300✔
1060
                                     dni.localaddr,
300✔
1061
                                     dni.di.zone.operator const DNSName&(),
300✔
1062
                                     QType::SOA,
300✔
1063
                                     nullptr,
300✔
1064
                                     dni.dnssecOk, dni.tsigkeyname, dni.tsigalgname, dni.tsigsecret)};
300✔
1065
    }
300✔
1066
    catch (PDNSException& e) {
300✔
1067
      throw runtime_error("While attempting to query freshness of '" + dni.di.zone.toLogString() + "': " + e.reason);
×
1068
    }
×
1069
  }
300✔
1070

1071
  bool receive(Identifier& id, Answer& a)
1072
  {
454✔
1073
    return d_resolver.tryGetSOASerial(&(std::get<0>(id)), &(std::get<1>(id)), &a.theirSerial, &a.theirInception, &a.theirExpire, &(std::get<2>(id)));
454✔
1074
  }
454✔
1075

1076
  void deliverAnswer(const DomainNotificationInfo& dni, const Answer& a, unsigned int /* usec */)
1077
  {
274✔
1078
    d_freshness[dni.di.id] = a;
274✔
1079
  }
274✔
1080

1081
  Resolver d_resolver;
1082
};
1083

1084
void CommunicatorClass::addSecondaryCheckRequest(const DomainInfo& di, const ComboAddress& remote)
1085
{
2✔
1086
  auto data = d_data.lock();
2✔
1087
  DomainInfo ours = di;
2✔
1088
  ours.backend = nullptr;
2✔
1089

1090
  // When adding a check, if the remote addr from which notification was
1091
  // received is a primary, clear all other primaries so we can be sure the
1092
  // query goes to that one.
1093
  for (const auto& primary : di.primaries) {
2!
1094
    if (ComboAddress::addressOnlyEqual()(remote, primary)) {
2!
1095
      ours.primaries.clear();
2✔
1096
      ours.primaries.push_back(primary);
2✔
1097
      break;
2✔
1098
    }
2✔
1099
  }
2✔
1100
  data->d_tocheck.erase(di);
2✔
1101
  data->d_tocheck.insert(ours);
2✔
1102
  d_any_sem.post(); // kick the loop!
2✔
1103
}
2✔
1104

1105
void CommunicatorClass::addTryAutoPrimaryRequest(const DNSPacket& p)
1106
{
4✔
1107
  const DNSPacket& ours = p;
4✔
1108
  auto data = d_data.lock();
4✔
1109
  if (data->d_potentialautoprimaries.insert(ours).second) {
4!
1110
    d_any_sem.post(); // kick the loop!
4✔
1111
  }
4✔
1112
}
4✔
1113

1114
void CommunicatorClass::secondaryRefresh(PacketHandler* P)
1115
{
133✔
1116
  // not unless we are secondary
1117
  if (!::arg().mustDo("secondary"))
133✔
1118
    return;
66✔
1119

1120
  UeberBackend* B = P->getBackend();
67✔
1121
  vector<DomainInfo> rdomains;
67✔
1122
  vector<DomainNotificationInfo> sdomains;
67✔
1123
  set<DNSPacket, Data::cmp> trysuperdomains;
67✔
1124
  {
67✔
1125
    auto data = d_data.lock();
67✔
1126
    set<DomainInfo> requeue;
67✔
1127
    rdomains.reserve(data->d_tocheck.size());
67✔
1128
    for (const auto& di : data->d_tocheck) {
67✔
1129
      if (data->d_inprogress.count(di.zone)) {
2!
1130
        g_log << Logger::Debug << "Got NOTIFY for " << di.zone << " while AXFR in progress, requeueing SOA check" << endl;
×
1131
        requeue.insert(di);
×
1132
      }
×
1133
      else {
2✔
1134
        // We received a NOTIFY for a zone. This means at least one of the zone's primary server is working.
1135
        // Therefore we delete the zone from the list of failed secondary-checks to allow immediate checking.
1136
        const auto wasFailedDomain = data->d_failedSecondaryRefresh.find(di.zone);
2✔
1137
        if (wasFailedDomain != data->d_failedSecondaryRefresh.end()) {
2!
1138
          g_log << Logger::Debug << "Got NOTIFY for " << di.zone << ", removing zone from list of failed secondary-checks and going to check SOA serial" << endl;
2✔
1139
          data->d_failedSecondaryRefresh.erase(di.zone);
2✔
1140
        }
2✔
1141
        else {
×
1142
          g_log << Logger::Debug << "Got NOTIFY for " << di.zone << ", going to check SOA serial" << endl;
×
1143
        }
×
1144
        rdomains.push_back(di);
2✔
1145
      }
2✔
1146
    }
2✔
1147
    data->d_tocheck.swap(requeue);
67✔
1148

1149
    trysuperdomains = std::move(data->d_potentialautoprimaries);
67✔
1150
    data->d_potentialautoprimaries.clear();
67✔
1151
  }
67✔
1152

1153
  for (const DNSPacket& dp : trysuperdomains) {
67✔
1154
    // get the TSIG key name
1155
    TSIGRecordContent trc;
4✔
1156
    DNSName tsigkeyname;
4✔
1157
    dp.getTSIGDetails(&trc, &tsigkeyname);
4✔
1158
    P->tryAutoPrimarySynchronous(dp, tsigkeyname); // FIXME could use some error logging
4✔
1159
  }
4✔
1160
  if (rdomains.empty()) { // if we have priority domains, check them first
67✔
1161
    B->getUnfreshSecondaryInfos(&rdomains);
65✔
1162
  }
65✔
1163
  sdomains.reserve(rdomains.size());
67✔
1164
  DNSSECKeeper dk(B); // NOW HEAR THIS! This DK uses our B backend, so no interleaved access!
67✔
1165
  bool checkSignatures = ::arg().mustDo("secondary-check-signature-freshness") && dk.doesDNSSEC();
67!
1166
  {
67✔
1167
    auto data = d_data.lock();
67✔
1168
    domains_by_name_t& nameindex = boost::multi_index::get<IDTag>(data->d_suckdomains);
67✔
1169
    time_t now = time(nullptr);
67✔
1170

1171
    for (DomainInfo& di : rdomains) {
306✔
1172
      const auto failed = data->d_failedSecondaryRefresh.find(di.zone);
306✔
1173
      if (failed != data->d_failedSecondaryRefresh.end() && now < failed->second.second) {
306!
1174
        // If the domain has failed before and the time before the next check has not expired, skip this domain
1175
        g_log << Logger::Debug << "Zone '" << di.zone << "' is on the list of failed SOA checks. Skipping SOA checks until " << failed->second.second << endl;
×
1176
        continue;
×
1177
      }
×
1178
      std::vector<std::string> localaddr;
306✔
1179
      SuckRequest sr;
306✔
1180
      sr.domain = di.zone;
306✔
1181
      if (di.primaries.empty()) // secondary domains w/o primaries are ignored
306!
1182
        continue;
×
1183
      // remove unfresh domains already queued for AXFR, no sense polling them again
1184
      sr.primary = *di.primaries.begin();
306✔
1185
      if (nameindex.count(sr)) { // this does NOT however protect us against AXFRs already in progress!
306!
1186
        continue;
×
1187
      }
×
1188
      if (data->d_inprogress.count(sr.domain)) { // this does
306✔
1189
        continue;
6✔
1190
      }
6✔
1191

1192
      DomainNotificationInfo dni;
300✔
1193
      dni.di = di;
300✔
1194
      dni.dnssecOk = checkSignatures;
300✔
1195

1196
      if (dk.getTSIGForAccess(di.zone, sr.primary, &dni.tsigkeyname)) {
300✔
1197
        string secret64;
25✔
1198
        if (!B->getTSIGKey(dni.tsigkeyname, dni.tsigalgname, secret64)) {
25!
1199
          g_log << Logger::Warning << "TSIG key '" << dni.tsigkeyname << "' for domain '" << di.zone << "' not found, can not AXFR." << endl;
×
1200
          continue;
×
1201
        }
×
1202
        if (B64Decode(secret64, dni.tsigsecret) == -1) {
25!
1203
          g_log << Logger::Error << "Unable to Base-64 decode TSIG key '" << dni.tsigkeyname << "' for domain '" << di.zone << "', can not AXFR." << endl;
×
1204
          continue;
×
1205
        }
×
1206
      }
25✔
1207

1208
      localaddr.clear();
300✔
1209
      // check for AXFR-SOURCE
1210
      if (B->getDomainMetadata(di.zone, "AXFR-SOURCE", localaddr) && !localaddr.empty()) {
300!
1211
        try {
22✔
1212
          dni.localaddr = ComboAddress(localaddr[0]);
22✔
1213
          g_log << Logger::Info << "Freshness check source (AXFR-SOURCE) for domain '" << di.zone << "' set to " << localaddr[0] << endl;
22✔
1214
        }
22✔
1215
        catch (std::exception& e) {
22✔
1216
          g_log << Logger::Error << "Failed to load freshness check source '" << localaddr[0] << "' for '" << di.zone << "': " << e.what() << endl;
×
1217
          return;
×
1218
        }
×
1219
      }
22✔
1220
      else {
278✔
1221
        dni.localaddr.sin4.sin_family = 0;
278✔
1222
      }
278✔
1223

1224
      sdomains.push_back(std::move(dni));
300✔
1225
    }
300✔
1226
  }
67✔
1227
  if (sdomains.empty()) {
67✔
1228
    if (d_secondarieschanged) {
15!
1229
      auto data = d_data.lock();
15✔
1230
      g_log << Logger::Info << "No new unfresh secondary domains, " << data->d_suckdomains.size() << " queued for AXFR already, " << data->d_inprogress.size() << " in progress" << endl;
15✔
1231
    }
15✔
1232
    d_secondarieschanged = !rdomains.empty();
15✔
1233
    return;
15✔
1234
  }
15✔
1235
  else {
52✔
1236
    auto data = d_data.lock();
52✔
1237
    g_log << Logger::Info << sdomains.size() << " secondary domain" << (sdomains.size() > 1 ? "s" : "") << " need" << (sdomains.size() > 1 ? "" : "s") << " checking, " << data->d_suckdomains.size() << " queued for AXFR" << endl;
52✔
1238
  }
52✔
1239

1240
  SecondarySenderReceiver ssr;
52✔
1241

1242
  Inflighter<vector<DomainNotificationInfo>, SecondarySenderReceiver> ifl(sdomains, ssr);
52✔
1243

1244
  ifl.d_maxInFlight = 200;
52✔
1245

1246
  for (;;) {
64✔
1247
    try {
64✔
1248
      ifl.run();
64✔
1249
      break;
64✔
1250
    }
64✔
1251
    catch (std::exception& e) {
64✔
1252
      g_log << Logger::Error << "While checking domain freshness: " << e.what() << endl;
×
1253
    }
×
1254
    catch (PDNSException& re) {
64✔
1255
      g_log << Logger::Error << "While checking domain freshness: " << re.reason << endl;
12✔
1256
    }
12✔
1257
  }
64✔
1258

1259
  if (ifl.getTimeouts()) {
64✔
1260
    g_log << Logger::Warning << "Received serial number updates for " << ssr.d_freshness.size() << " zone" << addS(ssr.d_freshness.size()) << ", had " << ifl.getTimeouts() << " timeout" << addS(ifl.getTimeouts()) << endl;
14✔
1261
  }
14✔
1262
  else {
50✔
1263
    g_log << Logger::Info << "Received serial number updates for " << ssr.d_freshness.size() << " zone" << addS(ssr.d_freshness.size()) << endl;
50✔
1264
  }
50✔
1265

1266
  time_t now = time(nullptr);
64✔
1267
  for (auto& val : sdomains) {
300✔
1268
    DomainInfo& di(val.di);
300✔
1269
    // If our di comes from packethandler (caused by incoming NOTIFY), di.backend will not be filled out,
1270
    // and di.serial will not either.
1271
    // Conversely, if our di came from getUnfreshSecondaryInfos, di.backend and di.serial are valid.
1272
    if (!di.backend) {
300✔
1273
      // Do not overwrite received DI just to make sure it exists in backend:
1274
      // di.primaries should contain the picked primary (as first entry)!
1275
      DomainInfo tempdi;
2✔
1276
      if (!B->getDomainInfo(di.zone, tempdi, false)) {
2!
1277
        g_log << Logger::Info << "Ignore domain " << di.zone << " since it has been removed from our backend" << endl;
×
1278
        continue;
×
1279
      }
×
1280
      // Backend for di still doesn't exist and this might cause us to
1281
      // SEGFAULT on the setFresh command later on
1282
      di.backend = tempdi.backend;
2✔
1283
    }
2✔
1284

1285
    if (!ssr.d_freshness.count(di.id)) { // If we don't have an answer for the domain
300✔
1286
      uint64_t newCount = 1;
26✔
1287
      auto data = d_data.lock();
26✔
1288
      const auto failedEntry = data->d_failedSecondaryRefresh.find(di.zone);
26✔
1289
      if (failedEntry != data->d_failedSecondaryRefresh.end())
26!
1290
        newCount = data->d_failedSecondaryRefresh[di.zone].first + 1;
×
1291
      time_t nextCheck = now + std::min(newCount * d_tickinterval, (uint64_t)::arg().asNum("default-ttl"));
26✔
1292
      data->d_failedSecondaryRefresh[di.zone] = {newCount, nextCheck};
26✔
1293
      if (newCount == 1) {
26!
1294
        g_log << Logger::Warning << "Unable to retrieve SOA for " << di.zone << ", this was the first time. NOTE: For every subsequent failed SOA check the domain will be suspended from freshness checks for 'num-errors x " << d_tickinterval << " seconds', with a maximum of " << (uint64_t)::arg().asNum("default-ttl") << " seconds. Skipping SOA checks until " << nextCheck << endl;
26✔
1295
      }
26✔
1296
      else if (newCount % 10 == 0) {
×
1297
        g_log << Logger::Notice << "Unable to retrieve SOA for " << di.zone << ", this was the " << std::to_string(newCount) << "th time. Skipping SOA checks until " << nextCheck << endl;
×
1298
      }
×
1299
      // Make sure we recheck SOA for notifies
1300
      if (di.receivedNotify) {
26!
1301
        di.backend->setStale(di.id);
×
1302
      }
×
1303
      continue;
26✔
1304
    }
26✔
1305

1306
    {
274✔
1307
      auto data = d_data.lock();
274✔
1308
      const auto wasFailedDomain = data->d_failedSecondaryRefresh.find(di.zone);
274✔
1309
      if (wasFailedDomain != data->d_failedSecondaryRefresh.end())
274!
1310
        data->d_failedSecondaryRefresh.erase(di.zone);
×
1311
    }
274✔
1312

1313
    bool hasSOA = false;
274✔
1314
    SOAData sd;
274✔
1315
    try {
274✔
1316
      // Use UeberBackend cache for SOA. Cache gets cleared after AXFR/IXFR.
1317
      B->lookup(QType(QType::SOA), di.zone.operator const DNSName&(), di.id, nullptr);
274✔
1318
      DNSZoneRecord zr;
274✔
1319
      hasSOA = B->get(zr);
274✔
1320
      if (hasSOA) {
274✔
1321
        fillSOAData(zr, sd);
26✔
1322
        while (B->get(zr))
26!
1323
          ;
×
1324
      }
26✔
1325
    }
274✔
1326
    catch (...) {
274✔
1327
    }
72✔
1328

1329
    uint32_t theirserial = ssr.d_freshness[di.id].theirSerial;
274✔
1330
    uint32_t ourserial = sd.serial;
274✔
1331
    const ComboAddress remote = *di.primaries.begin();
274✔
1332

1333
    if (hasSOA && rfc1982LessThan(theirserial, ourserial) && !::arg().mustDo("axfr-lower-serial")) {
274!
1334
      g_log << Logger::Warning << "Domain '" << di.zone << "' more recent than primary " << remote.toStringWithPortExcept(53) << ", our serial " << ourserial << " > their serial " << theirserial << endl;
×
1335
      di.backend->setFresh(di.id);
×
1336
    }
×
1337
    else if (hasSOA && theirserial == ourserial) {
274!
1338
      uint32_t maxExpire = 0, maxInception = 0;
26✔
1339
      if (checkSignatures && dk.isPresigned(di.zone)) {
26✔
1340
        B->lookup(QType(QType::RRSIG), di.zone.operator const DNSName&(), di.id); // can't use DK before we are done with this lookup!
9✔
1341
        DNSZoneRecord zr;
9✔
1342
        while (B->get(zr)) {
45✔
1343
          auto rrsig = getRR<RRSIGRecordContent>(zr.dr);
36✔
1344
          if (rrsig->d_type == QType::SOA) {
36✔
1345
            maxInception = std::max(maxInception, rrsig->d_siginception);
9✔
1346
            maxExpire = std::max(maxExpire, rrsig->d_sigexpire);
9✔
1347
          }
9✔
1348
        }
36✔
1349
      }
9✔
1350

1351
      SuckRequest::RequestPriority prio = SuckRequest::SignaturesRefresh;
26✔
1352
      if (di.receivedNotify) {
26✔
1353
        prio = SuckRequest::Notify;
2✔
1354
      }
2✔
1355

1356
      if (!maxInception && !ssr.d_freshness[di.id].theirInception) {
26!
1357
        g_log << Logger::Info << "Domain '" << di.zone << "' is fresh (no DNSSEC), serial is " << ourserial << " (checked primary " << remote.toStringWithPortExcept(53) << ")" << endl;
17✔
1358
        di.backend->setFresh(di.id);
17✔
1359
      }
17✔
1360
      else if (maxInception == ssr.d_freshness[di.id].theirInception && maxExpire == ssr.d_freshness[di.id].theirExpire) {
9!
1361
        g_log << Logger::Info << "Domain '" << di.zone << "' is fresh and SOA RRSIGs match, serial is " << ourserial << " (checked primary " << remote.toStringWithPortExcept(53) << ")" << endl;
9✔
1362
        di.backend->setFresh(di.id);
9✔
1363
      }
9✔
1364
      else if (maxExpire >= now && !ssr.d_freshness[di.id].theirInception) {
×
1365
        g_log << Logger::Info << "Domain '" << di.zone << "' is fresh, primary " << remote.toStringWithPortExcept(53) << " is no longer signed but (some) signatures are still valid, serial is " << ourserial << endl;
×
1366
        di.backend->setFresh(di.id);
×
1367
      }
×
1368
      else if (maxInception && !ssr.d_freshness[di.id].theirInception) {
×
1369
        g_log << Logger::Notice << "Domain '" << di.zone << "' is stale, primary " << remote.toStringWithPortExcept(53) << " is no longer signed and all signatures have expired, serial is " << ourserial << endl;
×
1370
        addSuckRequest(di.zone, remote, prio);
×
1371
      }
×
1372
      else if (dk.doesDNSSEC() && !maxInception && ssr.d_freshness[di.id].theirInception) {
×
1373
        g_log << Logger::Notice << "Domain '" << di.zone << "' is stale, primary " << remote.toStringWithPortExcept(53) << " has signed, serial is " << ourserial << endl;
×
1374
        addSuckRequest(di.zone, remote, prio);
×
1375
      }
×
1376
      else {
×
1377
        g_log << Logger::Notice << "Domain '" << di.zone << "' is fresh, but RRSIGs differ on primary " << remote.toStringWithPortExcept(53) << ", so DNSSEC is stale, serial is " << ourserial << endl;
×
1378
        addSuckRequest(di.zone, remote, prio);
×
1379
      }
×
1380
    }
26✔
1381
    else {
248✔
1382
      SuckRequest::RequestPriority prio = SuckRequest::SerialRefresh;
248✔
1383
      if (di.receivedNotify) {
248!
1384
        prio = SuckRequest::Notify;
×
1385
      }
×
1386

1387
      if (hasSOA) {
248!
1388
        g_log << Logger::Notice << "Domain '" << di.zone << "' is stale, primary " << remote.toStringWithPortExcept(53) << " serial " << theirserial << ", our serial " << ourserial << endl;
×
1389
      }
×
1390
      else {
248✔
1391
        g_log << Logger::Notice << "Domain '" << di.zone << "' is empty, primary " << remote.toStringWithPortExcept(53) << " serial " << theirserial << endl;
248✔
1392
      }
248✔
1393
      addSuckRequest(di.zone, remote, prio);
248✔
1394
    }
248✔
1395
  }
274✔
1396
}
64✔
1397

1398
vector<pair<ZoneName, ComboAddress>> CommunicatorClass::getSuckRequests()
1399
{
×
1400
  vector<pair<ZoneName, ComboAddress>> ret;
×
1401
  auto data = d_data.lock();
×
1402
  ret.reserve(data->d_suckdomains.size());
×
1403
  for (auto const& d : data->d_suckdomains) {
×
1404
    ret.emplace_back(d.domain, d.primary);
×
1405
  }
×
1406
  return ret;
×
1407
}
×
1408

1409
size_t CommunicatorClass::getSuckRequestsWaiting()
1410
{
9✔
1411
  return d_data.lock()->d_suckdomains.size();
9✔
1412
}
9✔
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