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

PowerDNS / pdns / 18370591226

09 Oct 2025 08:40AM UTC coverage: 64.094% (-0.04%) from 64.136%
18370591226

Pull #16224

github

web-flow
Merge b58891300 into 152db0df0
Pull Request #16224: dnsdist: Fix a typo in the XSK documentation

42757 of 101504 branches covered (42.12%)

Branch coverage included in aggregate %.

129859 of 167814 relevant lines covered (77.38%)

5755713.48 hits per line

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

66.67
/pdns/recursordist/lwres.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
#include "utility.hh"
26
#include "lwres.hh"
27
#include <iostream>
28
#include "dnsrecords.hh"
29
#include <cerrno>
30
#include "misc.hh"
31
#include <algorithm>
32
#include <sstream>
33
#include <cstring>
34
#include <string>
35
#include <vector>
36
#include "dns.hh"
37
#include "qtype.hh"
38
#include "pdnsexception.hh"
39
#include "arguments.hh"
40
#include "sstuff.hh"
41
#include "syncres.hh"
42
#include "dnswriter.hh"
43
#include "dnsparser.hh"
44
#include "logger.hh"
45
#include "dns_random.hh"
46
#include <boost/scoped_array.hpp>
47
#include <boost/algorithm/string.hpp>
48
#include "validate-recursor.hh"
49
#include "ednssubnet.hh"
50
#include "query-local-address.hh"
51
#include "tcpiohandler.hh"
52
#include "ednsoptions.hh"
53
#include "ednspadding.hh"
54
#include "rec-protozero.hh"
55
#include "uuid-utils.hh"
56
#include "rec-tcpout.hh"
57
#include "rec-cookiestore.hh"
58

59
static bool g_cookies = false;
60

61
std::string enableOutgoingCookies(bool flag, const string& unsupported)
62
{
178✔
63
  g_cookies = flag;
178✔
64
  if (g_cookies) {
178✔
65
    std::vector<std::string> parts;
3✔
66
    stringtok(parts, unsupported, ", ");
3✔
67
    std::string errors;
3✔
68
    addCookiesUnsupported(parts.begin(), parts.end(), errors);
3✔
69
    return errors;
3✔
70
  }
3✔
71
  return {};
175✔
72
}
178✔
73

74
thread_local TCPOutConnectionManager t_tcp_manager;
75
std::shared_ptr<Logr::Logger> g_slogout;
76
bool g_paddingOutgoing;
77
bool g_ECSHardening;
78

79
static LockGuarded<CookieStore> s_cookiestore;
80

81
uint64_t addCookiesUnsupported(vector<string>::iterator begin, vector<string>::iterator end, string& errors)
82
{
3✔
83
  auto lock = s_cookiestore.lock();
3✔
84
  uint64_t count = 0;
3✔
85
  while (begin != end) {
3!
86
    try {
×
87
      CookieEntry entry;
×
88
      entry.d_address = ComboAddress(*begin, 53);
×
89
      entry.setSupport(CookieEntry::Support::Unsupported, std::numeric_limits<time_t>::max());
×
90
      auto [iter, inserted] = lock->insert(entry);
×
91
      if (!inserted) {
×
92
        lock->replace(iter, entry);
×
93
      }
×
94
      ++count;
×
95
    }
×
96
    catch (const PDNSException& error) {
×
97
      if (!errors.empty()) {
×
98
        errors += ", ";
×
99
      }
×
100
      errors += error.reason;
×
101
    }
×
102
    ++begin;
×
103
  }
×
104
  return count;
3✔
105
}
3✔
106

107
uint64_t clearCookies(vector<string>::iterator begin, vector<string>::iterator end, string& errors)
108
{
6✔
109
  auto lock = s_cookiestore.lock();
6✔
110
  uint64_t count = 0;
6✔
111
  if (begin == end) {
6!
112
    return 0;
×
113
  }
×
114
  if (*begin == "*") {
6!
115
    count = lock->size();
6✔
116
    lock->clear();
6✔
117
  }
6✔
118
  else {
×
119
    while (begin != end) {
×
120
      try {
×
121
        count += lock->erase(ComboAddress(*begin, 53));
×
122
      }
×
123
      catch (const PDNSException& error) {
×
124
        if (!errors.empty()) {
×
125
          errors += ", ";
×
126
        }
×
127
        errors += error.reason;
×
128
      }
×
129
      ++begin;
×
130
    }
×
131
  }
×
132
  return count;
6✔
133
}
6✔
134

135
void pruneCookies(time_t cutoff)
136
{
113✔
137
  auto lock = s_cookiestore.lock();
113✔
138
  lock->prune(cutoff);
113✔
139
}
113✔
140

141
uint64_t dumpCookies(int fileDesc)
142
{
25✔
143
  CookieStore copy;
25✔
144
  {
25✔
145
    auto lock = s_cookiestore.lock();
25✔
146
    copy = *lock;
25✔
147
  }
25✔
148
  return copy.dump(fileDesc);
25✔
149
}
25✔
150

151
void remoteLoggerQueueData(RemoteLoggerInterface& rli, const std::string& data)
152
{
200✔
153
  auto ret = rli.queueData(data);
200✔
154

155
  switch (ret) {
200!
156
  case RemoteLoggerInterface::Result::Queued:
200!
157
    break;
200✔
158
  case RemoteLoggerInterface::Result::PipeFull: {
×
159
    const auto& msg = RemoteLoggerInterface::toErrorString(ret);
×
160
    g_slog->withName(rli.name())->info(Logr::Debug, msg);
×
161
    break;
×
162
  }
×
163
  case RemoteLoggerInterface::Result::TooLarge: {
×
164
    const auto& msg = RemoteLoggerInterface::toErrorString(ret);
×
165
    g_slog->withName(rli.name())->info(Logr::Debug, msg);
×
166
    break;
×
167
  }
×
168
  case RemoteLoggerInterface::Result::OtherError: {
×
169
    const auto& msg = RemoteLoggerInterface::toErrorString(ret);
×
170
    g_slog->withName(rli.name())->info(Logr::Warning, msg);
×
171
    break;
×
172
  }
×
173
  }
200✔
174
}
200✔
175

176
#include "dnstap.hh"
177

178
#ifdef HAVE_FSTRM
179
#include "fstrm_logger.hh"
180

181
static bool isEnabledForQueries(const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstreamLoggers)
182
{
10,723✔
183
  if (fstreamLoggers == nullptr) {
10,727✔
184
    return false;
10,723✔
185
  }
10,723✔
186
  for (auto& logger : *fstreamLoggers) {
2,147,483,651✔
187
    if (logger->logQueries()) {
4✔
188
      return true;
2✔
189
    }
2✔
190
  }
4✔
191
  return false;
2,147,483,649✔
192
}
2,147,483,651✔
193

194
static void logFstreamQuery(const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstreamLoggers, const struct timeval& queryTime, const ComboAddress& localip, const ComboAddress& address, DnstapMessage::ProtocolType protocol, const boost::optional<const DNSName&>& auth, const vector<uint8_t>& packet)
195
{
2✔
196
  if (fstreamLoggers == nullptr)
2!
197
    return;
×
198

199
  struct timespec ts;
2✔
200
  TIMEVAL_TO_TIMESPEC(&queryTime, &ts);
2✔
201
  std::string str;
2✔
202
  // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
203
  DnstapMessage message(std::move(str), DnstapMessage::MessageType::resolver_query, SyncRes::s_serverID, &localip, &address, protocol, reinterpret_cast<const char*>(packet.data()), packet.size(), &ts, nullptr, auth);
2✔
204
  str = message.getBuffer();
2✔
205

206
  for (auto& logger : *fstreamLoggers) {
2✔
207
    remoteLoggerQueueData(*logger, str);
2✔
208
  }
2✔
209
}
2✔
210

211
static bool isEnabledForResponses(const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstreamLoggers)
212
{
10,720✔
213
  if (fstreamLoggers == nullptr) {
10,725✔
214
    return false;
10,720✔
215
  }
10,720✔
216
  for (auto& logger : *fstreamLoggers) {
2,147,483,651✔
217
    if (logger->logResponses()) {
4!
218
      return true;
4✔
219
    }
4✔
220
  }
4✔
221
  return false;
2,147,483,647✔
222
}
2,147,483,651✔
223

224
static void logFstreamResponse(const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstreamLoggers, const ComboAddress& localip, const ComboAddress& address, DnstapMessage::ProtocolType protocol, const boost::optional<const DNSName&>& auth, const PacketBuffer& packet, const struct timeval& queryTime, const struct timeval& replyTime)
225
{
×
226
  if (fstreamLoggers == nullptr)
×
227
    return;
×
228

229
  struct timespec ts1, ts2;
×
230
  TIMEVAL_TO_TIMESPEC(&queryTime, &ts1);
×
231
  TIMEVAL_TO_TIMESPEC(&replyTime, &ts2);
×
232
  std::string str;
×
233
  // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
234
  DnstapMessage message(std::move(str), DnstapMessage::MessageType::resolver_response, SyncRes::s_serverID, &localip, &address, protocol, reinterpret_cast<const char*>(packet.data()), packet.size(), &ts1, &ts2, auth);
×
235
  str = message.getBuffer();
×
236

237
  for (auto& logger : *fstreamLoggers) {
×
238
    remoteLoggerQueueData(*logger, str);
×
239
  }
×
240
}
×
241

242
#endif // HAVE_FSTRM
243

244
static void logOutgoingQuery(const std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>>& outgoingLoggers, const boost::optional<const boost::uuids::uuid&>& initialRequestId, const boost::uuids::uuid& uuid, const ComboAddress& address, const DNSName& domain, int type, uint16_t qid, bool doTCP, bool tls, size_t bytes, const boost::optional<Netmask>& srcmask, const std::string& nsName)
245
{
22✔
246
  if (!outgoingLoggers) {
22!
247
    return;
×
248
  }
×
249

250
  bool log = false;
22✔
251
  for (auto& logger : *outgoingLoggers) {
29✔
252
    if (logger->logQueries()) {
29✔
253
      log = true;
15✔
254
      break;
15✔
255
    }
15✔
256
  }
29✔
257

258
  if (!log) {
22✔
259
    return;
7✔
260
  }
7✔
261

262
  static thread_local std::string buffer;
15✔
263
  buffer.clear();
15✔
264
  pdns::ProtoZero::Message m{buffer};
15✔
265
  m.setType(pdns::ProtoZero::Message::MessageType::DNSOutgoingQueryType);
15✔
266
  m.setMessageIdentity(uuid);
15✔
267
  m.setSocketFamily(address.sin4.sin_family);
15✔
268
  if (!doTCP) {
15!
269
    m.setSocketProtocol(pdns::ProtoZero::Message::TransportProtocol::UDP);
15✔
270
  }
15✔
271
  else if (!tls) {
×
272
    m.setSocketProtocol(pdns::ProtoZero::Message::TransportProtocol::TCP);
×
273
  }
×
274
  else {
×
275
    m.setSocketProtocol(pdns::ProtoZero::Message::TransportProtocol::DoT);
×
276
  }
×
277

278
  m.setTo(address);
15✔
279
  m.setInBytes(bytes);
15✔
280
  m.setTime();
15✔
281
  m.setId(qid);
15✔
282
  m.setQuestion(domain, type, QClass::IN);
15✔
283
  m.setToPort(address.getPort());
15✔
284
  m.setServerIdentity(SyncRes::s_serverID);
15✔
285

286
  if (initialRequestId) {
15!
287
    m.setInitialRequestID(*initialRequestId);
15✔
288
  }
15✔
289

290
  if (srcmask) {
15✔
291
    m.setEDNSSubnet(*srcmask, 128);
6✔
292
  }
6✔
293

294
  if (!nsName.empty()) {
15!
295
    m.setMeta("nsName", {nsName}, {});
15✔
296
  }
15✔
297
  for (auto& logger : *outgoingLoggers) {
30✔
298
    if (logger->logQueries()) {
30!
299
      remoteLoggerQueueData(*logger, buffer);
30✔
300
    }
30✔
301
  }
30✔
302
}
15✔
303

304
static void logIncomingResponse(const std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>>& outgoingLoggers, const boost::optional<const boost::uuids::uuid&>& initialRequestId, const boost::uuids::uuid& uuid, const ComboAddress& address, const DNSName& domain, int type, uint16_t qid, bool doTCP, bool tls, const boost::optional<Netmask>& srcmask, size_t bytes, int rcode, const std::vector<DNSRecord>& records, const struct timeval& queryTime, const std::set<uint16_t>& exportTypes, const std::string& nsName)
305
{
22✔
306
  if (!outgoingLoggers) {
22!
307
    return;
×
308
  }
×
309

310
  bool log = false;
22✔
311
  for (auto& logger : *outgoingLoggers) {
22!
312
    if (logger->logResponses()) {
22!
313
      log = true;
22✔
314
      break;
22✔
315
    }
22✔
316
  }
22✔
317

318
  if (!log) {
22!
319
    return;
×
320
  }
×
321

322
  static thread_local std::string buffer;
22✔
323
  buffer.clear();
22✔
324
  pdns::ProtoZero::RecMessage m{buffer};
22✔
325
  m.setType(pdns::ProtoZero::Message::MessageType::DNSIncomingResponseType);
22✔
326
  m.setMessageIdentity(uuid);
22✔
327
  m.setSocketFamily(address.sin4.sin_family);
22✔
328
  if (!doTCP) {
22!
329
    m.setSocketProtocol(pdns::ProtoZero::Message::TransportProtocol::UDP);
22✔
330
  }
22✔
331
  else if (!tls) {
×
332
    m.setSocketProtocol(pdns::ProtoZero::Message::TransportProtocol::TCP);
×
333
  }
×
334
  else {
×
335
    m.setSocketProtocol(pdns::ProtoZero::Message::TransportProtocol::DoT);
×
336
  }
×
337
  m.setTo(address);
22✔
338
  m.setInBytes(bytes);
22✔
339
  m.setTime();
22✔
340
  m.setId(qid);
22✔
341
  m.setQuestion(domain, type, QClass::IN);
22✔
342
  m.setToPort(address.getPort());
22✔
343
  m.setServerIdentity(SyncRes::s_serverID);
22✔
344

345
  if (initialRequestId) {
22!
346
    m.setInitialRequestID(*initialRequestId);
22✔
347
  }
22✔
348

349
  if (srcmask) {
22!
350
    m.setEDNSSubnet(*srcmask, 128);
×
351
  }
×
352
  if (!nsName.empty()) {
22!
353
    m.setMeta("nsName", {nsName}, {});
22✔
354
  }
22✔
355

356
  m.startResponse();
22✔
357
  m.setQueryTime(queryTime.tv_sec, queryTime.tv_usec);
22✔
358
  if (rcode == -1) {
22!
359
    m.setNetworkErrorResponseCode();
×
360
  }
×
361
  else {
22✔
362
    m.setResponseCode(rcode);
22✔
363
  }
22✔
364

365
  for (const auto& record : records) {
90✔
366
    m.addRR(record, exportTypes, std::nullopt);
90✔
367
  }
90✔
368
  m.commitResponse();
22✔
369

370
  for (auto& logger : *outgoingLoggers) {
44✔
371
    if (logger->logResponses()) {
44!
372
      remoteLoggerQueueData(*logger, buffer);
44✔
373
    }
44✔
374
  }
44✔
375
}
22✔
376

377
class BindError
378
{
379
};
380

381
static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std::optional<ComboAddress> localBind, TCPOutConnectionManager::Connection& connection, bool& dnsOverTLS, const std::string& nsName)
382
{
76✔
383
  dnsOverTLS = SyncRes::s_dot_to_port_853 && remote.getPort() == 853;
76!
384

385
  connection = t_tcp_manager.get({remote, localBind});
76✔
386
  if (connection.d_handler) {
76✔
387
    return false;
5✔
388
  }
5✔
389

390
  const struct timeval timeout{
71✔
391
    g_networkTimeoutMsec / 1000, static_cast<suseconds_t>(g_networkTimeoutMsec) % 1000 * 1000};
71✔
392
  Socket sock(remote.sin4.sin_family, SOCK_STREAM);
71✔
393
  sock.setNonBlocking();
71✔
394
  setTCPNoDelay(sock.getHandle());
71✔
395
  // Bind to the same address the cookie is associated with (RFC 9018 section 3 last paragraph)
396
  ComboAddress localip = localBind ? *localBind : pdns::getQueryLocalAddress(remote.sin4.sin_family, 0);
71✔
397
  if (localBind) {
71✔
398
    VLOG(log, "Connecting TCP to " << remote.toStringWithPortExcept(53) << " with specific local address " << localip.toString() << endl);
2!
399
  }
2✔
400
  else {
69✔
401
    VLOG(log, "Connecting TCP to " << remote.toStringWithPortExcept(53) << " with no specific local address" << endl);
69!
402
  }
69✔
403

404
  try {
71✔
405
    sock.bind(localip);
71✔
406
  }
71✔
407
  catch (const NetworkError& e) {
71✔
408
    if (localBind) {
×
409
      throw BindError();
×
410
    }
×
411
    throw;
×
412
  }
×
413

414
  std::shared_ptr<TLSCtx> tlsCtx{nullptr};
71✔
415
  if (dnsOverTLS) {
71✔
416
    TLSContextParameters tlsParams;
4✔
417
    tlsParams.d_provider = "openssl";
4✔
418
    tlsParams.d_validateCertificates = false;
4✔
419
    // tlsParams.d_caStore
420
    tlsCtx = getTLSContext(tlsParams);
4✔
421
    if (tlsCtx == nullptr) {
4!
422
      g_slogout->info(Logr::Error, "DoT requested but not available", "server", Logging::Loggable(remote));
×
423
      dnsOverTLS = false;
×
424
    }
×
425
  }
4✔
426
  connection.d_handler = std::make_shared<TCPIOHandler>(nsName, false, sock.releaseHandle(), timeout, tlsCtx);
71✔
427
  connection.d_local = localBind;
71✔
428
  // Returned state ignored
429
  // This can throw an exception, retry will need to happen at higher level
430
  connection.d_handler->tryConnect(SyncRes::s_tcp_fast_open_connect, remote);
71✔
431
  return true;
71✔
432
}
71✔
433

434
static LWResult::Result tcpsendrecv(const ComboAddress& ip, TCPOutConnectionManager::Connection& connection,
435
                                    ComboAddress& localip, const vector<uint8_t>& vpacket, size_t& len, PacketBuffer& buf)
436
{
76✔
437
  socklen_t slen = ip.getSocklen();
76✔
438
  uint16_t tlen = htons(vpacket.size());
76✔
439
  const char* lenP = reinterpret_cast<const char*>(&tlen);
76✔
440

441
  len = 0; // in case of error
76✔
442
  localip.sin4.sin_family = ip.sin4.sin_family;
76✔
443
  if (getsockname(connection.d_handler->getDescriptor(), reinterpret_cast<sockaddr*>(&localip), &slen) != 0) {
76!
444
    return LWResult::Result::PermanentError;
×
445
  }
×
446

447
  PacketBuffer packet;
76✔
448
  packet.reserve(2 + vpacket.size());
76✔
449
  packet.insert(packet.end(), lenP, lenP + 2);
76✔
450
  packet.insert(packet.end(), vpacket.begin(), vpacket.end());
76✔
451

452
  LWResult::Result ret = asendtcp(packet, connection.d_handler);
76✔
453
  if (ret != LWResult::Result::Success) {
76✔
454
    return ret;
2✔
455
  }
2✔
456

457
  ret = arecvtcp(packet, 2, connection.d_handler, false);
74✔
458
  if (ret != LWResult::Result::Success) {
74!
459
    return ret;
×
460
  }
×
461

462
  memcpy(&tlen, packet.data(), sizeof(tlen));
74✔
463
  len = ntohs(tlen); // switch to the 'len' shared with the rest of the calling function
74✔
464

465
  // XXX receive into buf directly?
466
  packet.resize(len);
74✔
467
  ret = arecvtcp(packet, len, connection.d_handler, false);
74✔
468
  if (ret != LWResult::Result::Success) {
74!
469
    return ret;
×
470
  }
×
471
  buf.resize(len);
74✔
472
  memcpy(buf.data(), packet.data(), len);
74✔
473
  return LWResult::Result::Success;
74✔
474
}
74✔
475

476
static void addPadding(const DNSPacketWriter& pw, size_t bufsize, DNSPacketWriter::optvect_t& opts)
477
{
9✔
478
  const size_t currentSize = pw.getSizeWithOpts(opts);
9✔
479
  if (currentSize < (bufsize - 4)) {
9!
480
    const size_t remaining = bufsize - (currentSize + 4);
9✔
481
    /* from rfc8647, "4.1.  Recommended Strategy: Block-Length Padding":
482
       Clients SHOULD pad queries to the closest multiple of 128 octets.
483
       Note we are in the client role here.
484
    */
485
    const size_t blockSize = 128;
9✔
486
    const size_t modulo = (currentSize + 4) % blockSize;
9✔
487
    size_t padSize = 0;
9✔
488
    if (modulo > 0) {
9!
489
      padSize = std::min(blockSize - modulo, remaining);
9✔
490
    }
9✔
491
    opts.emplace_back(EDNSOptionCode::PADDING, makeEDNSPaddingOptString(padSize));
9✔
492
  }
9✔
493
}
9✔
494

495
static void outgoingCookie(const OptLog& log, const ComboAddress& address, const timeval& now, DNSPacketWriter::optvect_t& opts, std::optional<EDNSCookiesOpt>& cookieSentOut, std::optional<ComboAddress>& addressToBindTo)
496
{
52✔
497
  auto lock = s_cookiestore.lock();
52✔
498
  if (auto found = lock->find(address); found != lock->end()) {
52✔
499
    switch (found->getSupport()) {
35!
500
    case CookieEntry::Support::Supported:
21✔
501
    case CookieEntry::Support::Probing:
22✔
502
      cookieSentOut = found->d_cookie;
22✔
503
      addressToBindTo = found->d_localaddress;
22✔
504
      opts.emplace_back(EDNSOptionCode::COOKIE, cookieSentOut->makeOptString());
22✔
505
      found->d_lastused = now.tv_sec;
22✔
506
      VLOG(log, "Sending stored cookie info to " << address.toString() << ": " << found->d_cookie.toDisplayString() << endl);
22!
507
      break;
22✔
508
    case CookieEntry::Support::Unsupported:
13✔
509
      VLOG(log, "Server " << address.toString() << " does not support cookies" << endl);
13!
510
      break;
13✔
511
    }
35✔
512
    return;
35✔
513
  }
35✔
514
  // Server not in table, it's either new or was purged
515
  CookieEntry entry;
17✔
516
  entry.d_address = address;
17✔
517
  entry.d_cookie.makeClientCookie();
17✔
518
  cookieSentOut = entry.d_cookie;
17✔
519
  entry.setSupport(CookieEntry::Support::Probing, now.tv_sec);
17✔
520
  lock->emplace(entry);
17✔
521
  opts.emplace_back(EDNSOptionCode::COOKIE, cookieSentOut->makeOptString());
17✔
522
  VLOG(log, "Sending new client cookie info to " << address.toString() << ": " << entry.d_cookie.toDisplayString() << endl);
17!
523
}
17✔
524

525
static std::pair<bool, LWResult::Result> incomingCookie(const OptLog& log, const ComboAddress& address, const ComboAddress& localip, const timeval& now, const std::optional<EDNSCookiesOpt>& cookieSentOut, const EDNSOpts& edo, bool doTCP, LWResult& lwr, bool& cookieFoundInReply)
526
{
52✔
527
  auto lock = s_cookiestore.lock();
52✔
528
  auto found = lock->find(address);
52✔
529

530
  if (found == lock->end()) {
52!
531
    // We received cookie (we might have sent one out) but the server is not in the table?
532
    // This is a case of cannot happen, unless rec_control clear-cookies was called
533
    VLOG(log, "Cookie from " << address.toString() << " not found back in table" << endl);
×
534
    lwr.d_rcode = RCode::FormErr;
×
535
    lwr.d_validpacket = false;
×
536
    return {true, LWResult::Result::Success}; // success - oddly enough
×
537
  }
×
538

539
  // We have stored cookie info, scan for COOKIE option in EDNS
540
  if (const auto opt = edo.getFirstOption(EDNSOptionCode::COOKIE); opt != edo.d_options.end()) {
52✔
541
    if (EDNSCookiesOpt received; received.makeFromString(opt->second)) {
33!
542
      cookieFoundInReply = true;
33✔
543
      VLOG(log, "Received cookie info back from " << address.toString() << ": " << received.toDisplayString() << endl);
33!
544
      if (received.getClient() == cookieSentOut->getClient()) {
33✔
545
        VLOG(log, "Client cookie from " << address.toString() << " matched! Storing with localAddress " << localip.toString() << endl);
31!
546
        ++t_Counters.at(rec::Counter::cookieMatched);
31✔
547
        found->d_localaddress = localip;
31✔
548
        found->d_localaddress.setPort(0);
31✔
549
        found->d_cookie = std::move(received);
31✔
550
        if (found->getSupport() == CookieEntry::Support::Probing) {
31✔
551
          ++t_Counters.at(rec::Counter::cookieProbeSupported);
10✔
552
        }
10✔
553
        found->setSupport(CookieEntry::Support::Supported, now.tv_sec);
31✔
554
        // check extended error code
555
        uint16_t ercode = edo.getCombinedERCode(lwr.d_rcode);
31✔
556
        if (ercode == ERCode::BADCOOKIE) {
31✔
557
          lwr.d_validpacket = true;
7✔
558
          ++t_Counters.at(rec::Counter::cookieRetry);
7✔
559
          VLOG(log, "Server " << localip.toString() << " returned BADCOOKIE " << endl);
7!
560
          return {true, LWResult::Result::BadCookie}; // We did update the entry, retry should succeed
7✔
561
        }
7✔
562
      }
31✔
563
      else {
2✔
564
        if (!doTCP) {
2✔
565
          // Server responded with a wrong client cookie, fall back to TCP, RFC 7873 5.3
566
          VLOG(log, "Server " << localip.toString() << " responded with wrong client cookie, fall back to TCP" << endl);
1!
567
          lwr.d_validpacket = true;
1✔
568
          ++t_Counters.at(rec::Counter::cookieMismatchedOverUDP);
1✔
569
          return {true, LWResult::Result::Spoofed};
1✔
570
        }
1✔
571
        // mismatched cookie when already doing TCP, ignore that
572
        VLOG(log, "Server " << localip.toString() << " responded with wrong client cookie over TCP, ignoring that" << endl);
1!
573
        ++t_Counters.at(rec::Counter::cookieMismatchedOverTCP);
1✔
574
      }
1✔
575
    }
33✔
576
    else {
×
577
      VLOG(log, "Malformed cookie in reply from " << address.toString() << ", dropping as if was a timeout" << endl);
×
578
      // Do something special if we get malformed repeatedly? And/or consider current status?
579
      lwr.d_validpacket = false;
×
580
      ++t_Counters.at(rec::Counter::cookieMalformed);
×
581
      return {true, LWResult::Result::Timeout};
×
582
    }
×
583
  } // COOKIE option found
33✔
584

585
  // The cases where something special needs to be done have been handled above
586
  return {false, LWResult::Result::Success};
44✔
587
}
52✔
588

589
/** lwr is only filled out in case 1 was returned, and even when returning 1 for 'success', lwr might contain DNS errors
590
    Never throws!
591
 */
592
// NOLINTNEXTLINE(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791
593
static LWResult::Result asyncresolve(const OptLog& log, const ComboAddress& address, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional<Netmask>& srcmask, const ResolveContext& context, const std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>>& outgoingLoggers, [[maybe_unused]] const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstrmLoggers, const std::set<uint16_t>& exportTypes, LWResult* lwr, bool* chained, TCPOutConnectionManager::Connection& connection)
594
{
10,728✔
595
  size_t len;
10,728✔
596
  size_t bufsize = g_outgoingEDNSBufsize;
10,728✔
597
  PacketBuffer buf;
10,728✔
598
  buf.resize(bufsize);
10,728✔
599
  vector<uint8_t> vpacket;
10,728✔
600
  //  string mapped0x20=dns0x20(domain);
601
  uint16_t qid = dns_random_uint16();
10,728✔
602
  DNSPacketWriter pw(vpacket, domain, type);
10,728✔
603
  bool dnsOverTLS = SyncRes::s_dot_to_port_853 && address.getPort() == 853;
10,728✔
604
  std::string nsName;
10,728✔
605
  if (!context.d_nsName.empty()) {
10,728✔
606
    nsName = context.d_nsName.toStringNoDot();
10,609✔
607
  }
10,609✔
608

609
  pw.getHeader()->rd = sendRDQuery;
10,728✔
610
  pw.getHeader()->id = qid;
10,728✔
611
  /* RFC 6840 section 5.9:
612
   *  This document further specifies that validating resolvers SHOULD set
613
   *  the CD bit on every upstream query.  This is regardless of whether
614
   *  the CD bit was set on the incoming query [...]
615
   *
616
   * sendRDQuery is only true if the qname is part of a forward-zone-recurse (or
617
   * set in the forward-zone-file), so we use this as an indicator for it being
618
   * an "upstream query". To stay true to "dnssec=off means 3.X behaviour", we
619
   * only set +CD on forwarded query in any mode other than dnssec=off.
620
   */
621
  pw.getHeader()->cd = (sendRDQuery && g_dnssecmode != DNSSECMode::Off);
10,728!
622

623
  std::optional<EDNSSubnetOpts> subnetOpts = std::nullopt;
10,728✔
624
  std::optional<ComboAddress> addressToBindTo;
10,728✔
625
  std::optional<EDNSCookiesOpt> cookieSentOut;
10,728✔
626

627
  if (EDNS0Level > 0) {
10,728✔
628
    DNSPacketWriter::optvect_t opts;
10,725✔
629
    if (srcmask) {
10,725✔
630
      subnetOpts = EDNSSubnetOpts{};
557✔
631
      subnetOpts->setSource(*srcmask);
557✔
632
      opts.emplace_back(EDNSOptionCode::ECS, subnetOpts->makeOptString());
557✔
633
    }
557✔
634

635
    if (g_cookies) {
10,725✔
636
      outgoingCookie(log, address, *now, opts, cookieSentOut, addressToBindTo);
52✔
637
    }
52✔
638

639
    if (dnsOverTLS && g_paddingOutgoing) {
10,725!
640
      addPadding(pw, bufsize, opts);
9✔
641
    }
9✔
642

643
    pw.addOpt(g_outgoingEDNSBufsize, 0, g_dnssecmode == DNSSECMode::Off ? 0 : EDNSOpts::DNSSECOK, opts);
10,725✔
644
    pw.commit();
10,725✔
645
  }
10,725✔
646
  lwr->d_rcode = 0;
10,728✔
647
  lwr->d_haveEDNS = false;
10,728✔
648
  LWResult::Result ret;
10,728✔
649

650
  DTime dt;
10,728✔
651
  dt.set();
10,728✔
652
  *now = dt.getTimeval();
10,728✔
653

654
  boost::uuids::uuid uuid;
10,728✔
655
  const struct timeval queryTime = *now;
10,728✔
656

657
  if (outgoingLoggers) {
10,728✔
658
    uuid = getUniqueID();
22✔
659
    logOutgoingQuery(outgoingLoggers, context.d_initialRequestId, uuid, address, domain, type, qid, doTCP, dnsOverTLS, vpacket.size(), srcmask, nsName);
22✔
660
  }
22✔
661

662
  srcmask = boost::none; // this is also our return value, even if EDNS0Level == 0
10,728✔
663

664
  // We only store the localip if needed for fstrm logging or cookie support
665
  ComboAddress localip;
10,728✔
666
  bool fstrmQEnabled = false;
10,728✔
667
  bool fstrmREnabled = false;
10,728✔
668

669
#ifdef HAVE_FSTRM
10,728✔
670
  if (isEnabledForQueries(fstrmLoggers)) {
10,728✔
671
    fstrmQEnabled = true;
2✔
672
  }
2✔
673
  if (isEnabledForResponses(fstrmLoggers)) {
10,728✔
674
    fstrmREnabled = true;
4✔
675
  }
4✔
676
#endif
10,728✔
677

678
  if (!doTCP) {
10,728✔
679
    int queryfd;
10,648✔
680
    try {
10,648✔
681
      ret = asendto(vpacket.data(), vpacket.size(), 0, address, addressToBindTo, qid, domain, type, subnetOpts, &queryfd, *now);
10,648✔
682
    }
10,648✔
683
    catch (const PDNSException& e) {
10,648✔
684
      if (addressToBindTo) {
×
685
        // Cookie info already has been added to packet, so we must retry from a higher level: SyncRes::asyncresolveWrapper
686
        auto lock = s_cookiestore.lock();
×
687
        lock->erase(address);
×
688
        return LWResult::Result::BindError;
×
689
      }
×
690
      throw;
×
691
    }
×
692
    if (ret != LWResult::Result::Success) {
10,648!
693
      return ret;
×
694
    }
×
695

696
    if (queryfd <= -1) {
10,648✔
697
      *chained = true;
1,556✔
698
    }
1,556✔
699

700
    if (!*chained) {
10,648✔
701
      if (cookieSentOut || fstrmQEnabled || fstrmREnabled) {
9,087✔
702
        localip.sin4.sin_family = address.sin4.sin_family;
41✔
703
        socklen_t slen = address.getSocklen();
41✔
704
        (void)getsockname(queryfd, reinterpret_cast<sockaddr*>(&localip), &slen); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast))
41✔
705
      }
41✔
706
#ifdef HAVE_FSTRM
9,086✔
707
      if (fstrmQEnabled) {
9,086✔
708
        logFstreamQuery(fstrmLoggers, queryTime, localip, address, DnstapMessage::ProtocolType::DoUDP, context.d_auth ? context.d_auth : boost::none, vpacket);
2!
709
      }
2✔
710
#endif
9,086✔
711
    }
9,086✔
712

713
    // sleep until we see an answer to this, interface to mtasker
714
    ret = arecvfrom(buf, 0, address, len, qid, domain, type, queryfd, subnetOpts, *now);
10,648✔
715
  }
10,648✔
716
  else {
80✔
717
    bool isNew{};
80✔
718
    do {
80✔
719
      try {
80✔
720
        // If we get a new (not re-used) TCP connection that does not
721
        // work, we give up. For reused connections, we assume the
722
        // peer has closed it on error, so we retry. At some point we
723
        // *will* get a new connection, so this loop is not endless.
724
        isNew = true; // tcpconnect() might throw for new connections. In that case, we want to break the loop, scanbuild complains here, which is a false positive afaik
80✔
725
        isNew = tcpconnect(log, address, addressToBindTo, connection, dnsOverTLS, nsName);
80✔
726
        ret = tcpsendrecv(address, connection, localip, vpacket, len, buf);
80✔
727
#ifdef HAVE_FSTRM
80✔
728
        if (fstrmQEnabled) {
80!
729
          logFstreamQuery(fstrmLoggers, queryTime, localip, address, !dnsOverTLS ? DnstapMessage::ProtocolType::DoTCP : DnstapMessage::ProtocolType::DoT, context.d_auth, vpacket);
×
730
        }
×
731
#endif /* HAVE_FSTRM */
80✔
732
        if (ret == LWResult::Result::Success) {
80✔
733
          break;
74✔
734
        }
74✔
735
        connection.d_handler->close();
6✔
736
      }
6✔
737
      catch (const BindError&) {
80✔
738
        // Cookie info already has been added to packet, so we must retry from a higher level
739
        auto lock = s_cookiestore.lock();
×
740
        lock->erase(address);
×
741
        return LWResult::Result::BindError;
×
742
      }
×
743
      catch (const NetworkError&) {
80✔
744
        ret = LWResult::Result::OSLimitError; // OS limits error
×
745
      }
×
746
      catch (const runtime_error&) {
80✔
747
        ret = LWResult::Result::OSLimitError; // OS limits error (PermanentError is transport related)
×
748
      }
×
749
    } while (!isNew);
80!
750
  }
80✔
751

752
  lwr->d_usec = dt.udiff();
10,724✔
753
  *now = dt.getTimeval();
10,724✔
754

755
  if (ret != LWResult::Result::Success) { // includes 'timeout'
10,724✔
756
    if (outgoingLoggers) {
336!
757
      logIncomingResponse(outgoingLoggers, context.d_initialRequestId, uuid, address, domain, type, qid, doTCP, dnsOverTLS, srcmask, 0, -1, {}, queryTime, exportTypes, nsName);
×
758
    }
×
759
    return ret;
336✔
760
  }
336✔
761

762
  if (*chained) {
10,388✔
763
    auto msec = lwr->d_usec / 1000;
1,319✔
764
    if (msec > g_networkTimeoutMsec * 2 / 3) {
1,319✔
765
      auto jitterMsec = dns_random(msec);
2✔
766
      if (jitterMsec > 0) {
2!
767
        mthreadSleep(jitterMsec);
2✔
768
      }
2✔
769
    }
2✔
770
  }
1,319✔
771

772
  buf.resize(len);
10,388✔
773

774
#ifdef HAVE_FSTRM
10,388✔
775
  if (fstrmREnabled && (!*chained || doTCP)) {
10,388!
776
    DnstapMessage::ProtocolType protocol = doTCP ? DnstapMessage::ProtocolType::DoTCP : DnstapMessage::ProtocolType::DoUDP;
×
777
    if (dnsOverTLS) {
×
778
      protocol = DnstapMessage::ProtocolType::DoT;
×
779
    }
×
780
    logFstreamResponse(fstrmLoggers, localip, address, protocol, context.d_auth, buf, queryTime, *now);
×
781
  }
×
782
#endif /* HAVE_FSTRM */
10,388✔
783

784
  lwr->d_records.clear();
10,388✔
785
  try {
10,388✔
786
    lwr->d_tcbit = 0;
10,388✔
787
    MOADNSParser mdp(false, reinterpret_cast<const char*>(buf.data()), buf.size());
10,388✔
788
    lwr->d_aabit = mdp.d_header.aa;
10,388✔
789
    lwr->d_tcbit = mdp.d_header.tc;
10,388✔
790
    lwr->d_rcode = mdp.d_header.rcode;
10,388✔
791

792
    if (mdp.d_header.rcode == RCode::FormErr && mdp.d_qname.empty() && mdp.d_qtype == 0 && mdp.d_qclass == 0) {
10,388!
793
      if (outgoingLoggers) {
×
794
        logIncomingResponse(outgoingLoggers, context.d_initialRequestId, uuid, address, domain, type, qid, doTCP, dnsOverTLS, srcmask, len, lwr->d_rcode, lwr->d_records, queryTime, exportTypes, nsName);
×
795
      }
×
796
      lwr->d_validpacket = true;
×
797
      return LWResult::Result::Success; // this is "success", the error is set in lwr->d_rcode
×
798
    }
×
799

800
    if (domain != mdp.d_qname) {
10,388!
801
      if (!mdp.d_qname.empty() && domain.toString().find((char)0) == string::npos /* ugly */) { // embedded nulls are too noisy, plus empty domains are too
×
802
        g_slogout->info(Logr::Notice, "Packet purporting to come from remote server contained wrong answer",
×
803
                        "server", Logging::Loggable(address),
×
804
                        "qname", Logging::Loggable(domain),
×
805
                        "onwire", Logging::Loggable(mdp.d_qname));
×
806
      }
×
807
      // unexpected count has already been done @ pdns_recursor.cc
808
      goto out;
×
809
    }
×
810

811
    lwr->d_records.reserve(mdp.d_answers.size());
10,388✔
812
    for (const auto& answer : mdp.d_answers) {
67,019✔
813
      lwr->d_records.push_back(answer);
67,019✔
814
    }
67,019✔
815

816
    bool cookieFoundInReply = false;
10,388✔
817
    if (EDNSOpts edo; EDNS0Level > 0 && getEDNSOpts(mdp, &edo)) {
10,392✔
818
      lwr->d_haveEDNS = true;
10,390✔
819

820
      // If we sent out ECS, we can also expect to see a return with or without ECS, the absent case
821
      // is not handled explicitly. If we do see a ECS in the reply, the source part *must* match
822
      // with what we sent out. See https://www.rfc-editor.org/rfc/rfc7871#section-7.3. and section
823
      // 11.2.
824
      // For ECS hardening mode, the case where we sent out an ECS but did not receive a matching
825
      // one is handled in arecvfrom().
826
      if (subnetOpts) {
10,390✔
827
        // THE RFC is not clear about the case of having multiple ECS options. We only look at the first.
828
        if (const auto opt = edo.getFirstOption(EDNSOptionCode::ECS); opt != edo.d_options.end()) {
291✔
829
          EDNSSubnetOpts reso;
52✔
830
          if (EDNSSubnetOpts::getFromString(opt->second, &reso)) {
52!
831
            if (!doTCP && reso.getSource() != subnetOpts->getSource()) {
52!
832
              g_slogout->info(Logr::Notice, "Incoming ECS does not match outgoing",
1✔
833
                              "server", Logging::Loggable(address),
1✔
834
                              "qname", Logging::Loggable(domain),
1✔
835
                              "outgoing", Logging::Loggable(subnetOpts->getSource()),
1✔
836
                              "incoming", Logging::Loggable(reso.getSource()));
1✔
837
              return LWResult::Result::Spoofed;
1✔
838
            }
1✔
839
            /* rfc7871 states that 0 "indicate[s] that the answer is suitable for all addresses in FAMILY",
840
               so we might want to still pass the information along to be able to differentiate between
841
               IPv4 and IPv6. Still I'm pretty sure it doesn't matter in real life, so let's not duplicate
842
               entries in our cache. */
843
            if (reso.getScopePrefixLength() != 0) {
51!
844
              uint8_t bits = std::min(reso.getScopePrefixLength(), subnetOpts->getSourcePrefixLength());
51✔
845
              auto outgoingECSAddr = subnetOpts->getSource().getNetwork();
51✔
846
              outgoingECSAddr.truncate(bits);
51✔
847
              srcmask = Netmask(outgoingECSAddr, bits);
51✔
848
            }
51✔
849
          }
51✔
850
        }
52✔
851
      }
291✔
852
      if (g_cookies && !*chained) {
10,389!
853
        auto [done, result] = incomingCookie(log, address, localip, *now, cookieSentOut, edo, doTCP, *lwr, cookieFoundInReply);
52✔
854
        if (done) {
52✔
855
          return result;
8✔
856
        }
8✔
857
      }
52✔
858
    }
10,389✔
859

860
    // Case: we sent out a cookie but did not get one back
861
    if (cookieSentOut && !cookieFoundInReply && !*chained) {
10,379!
862
      ++t_Counters.at(rec::Counter::cookieNotInReply);
6✔
863
      auto lock = s_cookiestore.lock();
6✔
864
      auto found = lock->find(address);
6✔
865
      if (found != lock->end()) {
6!
866
        switch (found->getSupport()) {
6!
867
        case CookieEntry::Support::Probing:
6!
868
          VLOG(log, "No cookie in reply from " << address.toString() << ", was probing, setting support to Unsupported" << endl);
6!
869
          found->setSupport(CookieEntry::Support::Unsupported, now->tv_sec);
6✔
870
          ++t_Counters.at(rec::Counter::cookieProbeUnsupported);
6✔
871
          break;
6✔
872
        case CookieEntry::Support::Unsupported:
×
873
          // We could have detected the server does not support cookies in the meantime
874
          VLOG(log, "No cookie in reply from " << address.toString() << ", cookie state is Unsupported, fine" << endl);
×
875
          break;
×
876
        case CookieEntry::Support::Supported:
×
877
          // RFC says: ignore replies not containing any cookie info, equivalent to timeout
878
          VLOG(log, "No cookie in reply from " << address.toString() << ", cookie state is Supported, dropping packet as if it timed out)" << endl);
×
879
          return LWResult::Result::Timeout;
×
880
          break;
×
881
        }
6✔
882
      }
6✔
883
      else {
×
884
        VLOG(log, "No cookie in reply from " << address.toString() << ", cookie state is Unknown, dropping packet as if it timed out" << endl);
×
885
        return LWResult::Result::Timeout;
×
886
      }
×
887
    }
6✔
888

889
    if (outgoingLoggers) {
10,379✔
890
      logIncomingResponse(outgoingLoggers, context.d_initialRequestId, uuid, address, domain, type, qid, doTCP, dnsOverTLS, srcmask, len, lwr->d_rcode, lwr->d_records, queryTime, exportTypes, nsName);
22✔
891
    }
22✔
892

893
    lwr->d_validpacket = true;
10,379✔
894
    return LWResult::Result::Success;
10,379✔
895
  }
10,379✔
896
  catch (const std::exception& mde) {
10,388✔
897
    if (::arg().mustDo("log-common-errors")) {
×
898
      g_slogout->error(Logr::Notice, mde.what(), "Unable to parse packet from remote server", "server", Logging::Loggable(address),
×
899
                       "exception", Logging::Loggable("std::exception"));
×
900
    }
×
901

902
    lwr->d_rcode = RCode::FormErr;
×
903
    lwr->d_validpacket = false;
×
904
    t_Counters.at(rec::Counter::serverParseError)++;
×
905

906
    if (outgoingLoggers) {
×
907
      logIncomingResponse(outgoingLoggers, context.d_initialRequestId, uuid, address, domain, type, qid, doTCP, dnsOverTLS, srcmask, len, lwr->d_rcode, lwr->d_records, queryTime, exportTypes, nsName);
×
908
    }
×
909

910
    return LWResult::Result::Success; // success - oddly enough
×
911
  }
×
912
  catch (...) {
10,388✔
913
    g_slogout->info(Logr::Notice, "Unknown error parsing packet from remote server", "server", Logging::Loggable(address));
×
914
  }
×
915

916
  t_Counters.at(rec::Counter::serverParseError)++;
×
917

918
out:
×
919
  if (!lwr->d_rcode) {
×
920
    lwr->d_rcode = RCode::ServFail;
×
921
  }
×
922

923
  return LWResult::Result::PermanentError;
×
924
}
×
925

926
LWResult::Result asyncresolve(const OptLog& log, const ComboAddress& address, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional<Netmask>& srcmask, const ResolveContext& context, const std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>>& outgoingLoggers, const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstrmLoggers, const std::set<uint16_t>& exportTypes, LWResult* lwr, bool* chained)
927
{
10,728✔
928
  TCPOutConnectionManager::Connection connection;
10,728✔
929
  auto ret = asyncresolve(log, address, domain, type, doTCP, sendRDQuery, EDNS0Level, now, srcmask, context, outgoingLoggers, fstrmLoggers, exportTypes, lwr, chained, connection);
10,728✔
930

931
  if (doTCP) {
10,728✔
932
    if (connection.d_handler && lwr->d_validpacket) {
76!
933
      t_tcp_manager.store(*now, std::make_pair(address, connection.d_local), std::move(connection));
74✔
934
    }
74✔
935
  }
76✔
936
  return ret;
10,728✔
937
}
10,728✔
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