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

PowerDNS / pdns / 18743945403

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

Pull #16356

github

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

42073 of 92452 branches covered (45.51%)

Branch coverage included in aggregate %.

128008 of 165855 relevant lines covered (77.18%)

6379935.17 hits per line

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

65.42
/pdns/recursordist/rec-tcp.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

23
#include "rec-main.hh"
24

25
#include "arguments.hh"
26
#include "logger.hh"
27
#include "mplexer.hh"
28
#include "uuid-utils.hh"
29

30
// OLD PRE 5.0.0 situation:
31
//
32
// When pdns-distributes-queries is false with reuseport true (the default since 4.9.0), TCP queries
33
// are read and handled by worker threads. If the kernel balancing is OK for TCP sockets (observed
34
// to be good on Debian bullseye, but not good on e.g. MacOS), the TCP handling is no extra burden.
35
// In the case of MacOS all incoming TCP queries are handled by a single worker, while incoming UDP
36
// queries do get distributed round-robin over the worker threads.  Do note the TCP queries might
37
// need to wait until the g_maxUDPQueriesPerRound is reached.
38
//
39
// In the case of pdns-distributes-queries true and reuseport false the queries were read and
40
// initially processed by the distributor thread(s).
41
//
42
// Initial processing consist of parsing, calling gettag and checking if we have a packet cache
43
// hit. If that does not produce a hit, the query is passed to an mthread in the same way as with
44
// UDP queries, but do note that the mthread processing is serviced by the distributor thread. The
45
// final answer will be sent by the same distributor thread that originally picked up the query.
46
//
47
// Changing this, and having incoming TCP queries handled by worker threads is somewhat more complex
48
// than UDP, as the socket must remain available in the distributor thread (for reading more
49
// queries), but the TCP socket must also be passed to a worker thread so it can write its
50
// answer. The in-flight bookkeeping also has to be aware of how a query is handled to do the
51
// accounting properly. I am not sure if changing the current setup is worth all this trouble,
52
// especially since the default is now to not use pdns-distributes-queries, which works well in many
53
// cases.
54
//
55
// NEW SITUATION SINCE 5.0.0:
56
//
57
// The drawback mentioned in https://github.com/PowerDNS/pdns/issues/8394 are not longer true, so an
58
// alternative approach would be to introduce dedicated TCP worker thread(s).
59
//
60
// This approach was implemented in https://github.com/PowerDNS/pdns/pull/13195. The distributor and
61
// worker thread(s) now no longer process TCP queries.
62

63
size_t g_tcpMaxQueriesPerConn;
64
unsigned int g_maxTCPClients;
65
unsigned int g_maxTCPPerClient;
66
int g_tcpTimeout;
67
bool g_anyToTcp;
68

69
uint16_t TCPConnection::s_maxInFlight;
70

71
using tcpClientCounts_t = map<ComboAddress, uint32_t, ComboAddress::addressOnlyLessThan>;
72
static thread_local std::unique_ptr<tcpClientCounts_t> t_tcpClientCounts = std::make_unique<tcpClientCounts_t>();
73

74
static void handleRunningTCPQuestion(int fileDesc, FDMultiplexer::funcparam_t& var);
75

76
#if 0
77
#define TCPLOG(tcpsock, x)                                 \
78
  do {                                                     \
79
    cerr << []() { timeval t; gettimeofday(&t, nullptr); return t.tv_sec % 10  + t.tv_usec/1000000.0; }() << " FD " << (tcpsock) << ' ' << x; \
80
  } while (0)
81
#else
82
// We do not define this as empty since that produces a duplicate case label warning from clang-tidy
83
#define TCPLOG(pid, x) /* NOLINT(cppcoreguidelines-macro-usage) */ \
84
  while (false) {                                                  \
562✔
85
    cerr << x; /* NOLINT(bugprone-macro-parentheses) */            \
×
86
  }
×
87
#endif
88

89
std::atomic<uint32_t> TCPConnection::s_currentConnections;
90

91
TCPConnection::TCPConnection(int fileDesc, const ComboAddress& addr) :
92
  data(2, 0), d_remote(addr), d_fd(fileDesc)
93
{
404✔
94
  ++s_currentConnections;
404✔
95
  (*t_tcpClientCounts)[d_remote]++;
404✔
96
}
404✔
97

98
TCPConnection::~TCPConnection()
99
{
404✔
100
  try {
404✔
101
    if (closesocket(d_fd) < 0) {
404!
102
      SLOG(g_log << Logger::Error << "Error closing socket for TCPConnection" << endl,
×
103
           g_slogtcpin->info(Logr::Error, "Error closing socket for TCPConnection"));
×
104
    }
×
105
  }
404✔
106
  catch (const PDNSException& e) {
404✔
107
    SLOG(g_log << Logger::Error << "Error closing TCPConnection socket: " << e.reason << endl,
×
108
         g_slogtcpin->error(Logr::Error, e.reason, "Error closing TCPConnection socket", "exception", Logging::Loggable("PDNSException")));
×
109
  }
×
110

111
  if (t_tcpClientCounts && t_tcpClientCounts->count(d_remote) != 0 && (*t_tcpClientCounts)[d_remote]-- == 0) {
404!
112
    t_tcpClientCounts->erase(d_remote);
×
113
  }
×
114
  --s_currentConnections;
404✔
115
}
404✔
116

117
static void terminateTCPConnection(int fileDesc)
118
{
401✔
119
  try {
401✔
120
    t_fdm->removeReadFD(fileDesc);
401✔
121
  }
401✔
122
  catch (const FDMultiplexerException& fde) {
401✔
123
  }
×
124
}
401✔
125

126
static void sendErrorOverTCP(std::unique_ptr<DNSComboWriter>& comboWriter, int rcode)
127
{
×
128
  std::vector<uint8_t> packet;
×
129
  if (comboWriter->d_mdp.d_header.qdcount == 0U) {
×
130
    /* header-only */
131
    packet.resize(sizeof(dnsheader));
×
132
  }
×
133
  else {
×
134
    DNSPacketWriter packetWriter(packet, comboWriter->d_mdp.d_qname, comboWriter->d_mdp.d_qtype, comboWriter->d_mdp.d_qclass);
×
135
    if (comboWriter->d_mdp.hasEDNS()) {
×
136
      /* we try to add the EDNS OPT RR even for truncated answers,
137
         as rfc6891 states:
138
         "The minimal response MUST be the DNS header, question section, and an
139
         OPT record.  This MUST also occur when a truncated response (using
140
         the DNS header's TC bit) is returned."
141
      */
142
      packetWriter.addOpt(512, 0, 0);
×
143
      packetWriter.commit();
×
144
    }
×
145
  }
×
146

147
  auto& header = reinterpret_cast<dnsheader&>(packet.at(0)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) safe cast
×
148
  header.aa = 0;
×
149
  header.ra = 1;
×
150
  header.qr = 1;
×
151
  header.tc = 0;
×
152
  header.id = comboWriter->d_mdp.d_header.id;
×
153
  header.rd = comboWriter->d_mdp.d_header.rd;
×
154
  header.cd = comboWriter->d_mdp.d_header.cd;
×
155
  header.rcode = rcode;
×
156

157
  sendResponseOverTCP(comboWriter, packet);
×
158
}
×
159

160
void finishTCPReply(std::unique_ptr<DNSComboWriter>& comboWriter, bool hadError, bool updateInFlight)
161
{
850✔
162
  // update tcp connection status, closing if needed and doing the fd multiplexer accounting
163
  if (updateInFlight && comboWriter->d_tcpConnection->d_requestsInFlight > 0) {
850!
164
    comboWriter->d_tcpConnection->d_requestsInFlight--;
784✔
165
  }
784✔
166

167
  // In the code below, we try to remove the fd from the set, but
168
  // we don't know if another mthread already did the remove, so we can get a
169
  // "Tried to remove unlisted fd" exception.  Not that an inflight < limit test
170
  // will not work since we do not know if the other mthread got an error or not.
171
  if (hadError) {
850!
172
    terminateTCPConnection(comboWriter->d_socket);
×
173
    comboWriter->d_socket = -1;
×
174
    return;
×
175
  }
×
176
  comboWriter->d_tcpConnection->queriesCount++;
850✔
177
  if ((g_tcpMaxQueriesPerConn > 0 && comboWriter->d_tcpConnection->queriesCount >= g_tcpMaxQueriesPerConn) || (comboWriter->d_tcpConnection->isDropOnIdle() && comboWriter->d_tcpConnection->d_requestsInFlight == 0)) {
850!
178
    try {
3✔
179
      t_fdm->removeReadFD(comboWriter->d_socket);
3✔
180
    }
3✔
181
    catch (FDMultiplexerException&) {
3✔
182
    }
×
183
    comboWriter->d_socket = -1;
3✔
184
    return;
3✔
185
  }
3✔
186

187
  Utility::gettimeofday(&g_now, nullptr); // needs to be updated
847✔
188
  struct timeval ttd = g_now;
847✔
189

190
  // If we cross from max to max-1 in flight requests, the fd was not listened to, add it back
191
  if (updateInFlight && comboWriter->d_tcpConnection->d_requestsInFlight == TCPConnection::s_maxInFlight - 1) {
847✔
192
    // A read error might have happened. If we add the fd back, it will most likely error again.
193
    // This is not a big issue, the next handleTCPClientReadable() will see another read error
194
    // and take action.
195
    ttd.tv_sec += g_tcpTimeout;
2✔
196
    t_fdm->addReadFD(comboWriter->d_socket, handleRunningTCPQuestion, comboWriter->d_tcpConnection, &ttd);
2✔
197
    return;
2✔
198
  }
2✔
199
  // fd might have been removed by read error code, or a read timeout, so expect an exception
200
  try {
845✔
201
    t_fdm->setReadTTD(comboWriter->d_socket, ttd, g_tcpTimeout);
845✔
202
  }
845✔
203
  catch (const FDMultiplexerException&) {
845✔
204
    // but if the FD was removed because of a timeout while we were sending a response,
205
    // we need to re-arm it. If it was an error it will error again.
206
    ttd.tv_sec += g_tcpTimeout;
×
207
    t_fdm->addReadFD(comboWriter->d_socket, handleRunningTCPQuestion, comboWriter->d_tcpConnection, &ttd);
×
208
  }
×
209
}
845✔
210

211
/*
212
 * A helper class that by default closes the incoming TCP connection on destruct
213
 * If you want to keep the connection alive, call keep() on the guard object
214
 */
215
class RunningTCPQuestionGuard
216
{
217
public:
218
  RunningTCPQuestionGuard(const RunningTCPQuestionGuard&) = default;
219
  RunningTCPQuestionGuard(RunningTCPQuestionGuard&&) = delete;
220
  RunningTCPQuestionGuard& operator=(const RunningTCPQuestionGuard&) = default;
221
  RunningTCPQuestionGuard& operator=(RunningTCPQuestionGuard&&) = delete;
222
  RunningTCPQuestionGuard(int fileDesc) :
223
    d_fd(fileDesc) {}
1,565✔
224
  ~RunningTCPQuestionGuard()
225
  {
1,565✔
226
    if (d_fd != -1) {
1,565✔
227
      terminateTCPConnection(d_fd);
401✔
228
      d_fd = -1;
401✔
229
    }
401✔
230
  }
1,565✔
231
  void keep()
232
  {
2,014✔
233
    d_fd = -1;
2,014✔
234
  }
2,014✔
235
  bool handleTCPReadResult(int /* fd */, ssize_t bytes)
236
  {
417✔
237
    if (bytes == 0) {
417✔
238
      /* EOF */
239
      return false;
391✔
240
    }
391✔
241
    if (bytes < 0) {
26!
242
      if (errno != EAGAIN && errno != EWOULDBLOCK) {
26!
243
        return false;
×
244
      }
×
245
    }
26✔
246
    keep();
26✔
247
    return true;
26✔
248
  }
26✔
249

250
private:
251
  int d_fd{-1};
252
};
253

254
static void handleNotify(std::unique_ptr<DNSComboWriter>& comboWriter, const DNSName& qname)
255
{
1✔
256
  if (!t_allowNotifyFrom || !t_allowNotifyFrom->match(comboWriter->d_mappedSource)) {
1!
257
    if (!g_quiet) {
×
258
      SLOG(g_log << Logger::Error << "[" << g_multiTasker->getTid() << "] dropping TCP NOTIFY from " << comboWriter->d_mappedSource.toString() << ", address not matched by allow-notify-from" << endl,
×
259
           g_slogtcpin->info(Logr::Error, "Dropping TCP NOTIFY, address not matched by allow-notify-from", "source", Logging::Loggable(comboWriter->d_mappedSource)));
×
260
    }
×
261

262
    t_Counters.at(rec::Counter::sourceDisallowedNotify)++;
×
263
    return;
×
264
  }
×
265

266
  if (!isAllowNotifyForZone(qname)) {
1!
267
    if (!g_quiet) {
×
268
      SLOG(g_log << Logger::Error << "[" << g_multiTasker->getTid() << "] dropping TCP NOTIFY from " << comboWriter->d_mappedSource.toString() << ", for " << qname.toLogString() << ", zone not matched by allow-notify-for" << endl,
×
269
           g_slogtcpin->info(Logr::Error, "Dropping TCP NOTIFY,  zone not matched by allow-notify-for", "source", Logging::Loggable(comboWriter->d_mappedSource), "zone", Logging::Loggable(qname)));
×
270
    }
×
271

272
    t_Counters.at(rec::Counter::zoneDisallowedNotify)++;
×
273
    return;
×
274
  }
×
275
}
1✔
276

277
static void doProtobufLogQuery(bool logQuery, LocalStateHolder<LuaConfigItems>& luaconfsLocal, const std::unique_ptr<DNSComboWriter>& comboWriter, const DNSName& qname, QType qtype, QClass qclass, const dnsheader* dnsheader, const shared_ptr<TCPConnection>& conn, const boost::optional<uint32_t>& ednsVersion)
278
{
8✔
279
  try {
8✔
280
    if (logQuery && !(luaconfsLocal->protobufExportConfig.taggedOnly && comboWriter->d_policyTags.empty())) {
8!
281
      protobufLogQuery(luaconfsLocal, comboWriter->d_uuid, comboWriter->d_source, comboWriter->d_destination, comboWriter->d_mappedSource, comboWriter->d_ednssubnet.getSource(), true, conn->qlen, qname, qtype, qclass, comboWriter->d_policyTags, comboWriter->d_requestorId, comboWriter->d_deviceId, comboWriter->d_deviceName, comboWriter->d_meta, ednsVersion, *dnsheader);
2✔
282
    }
2✔
283
  }
8✔
284
  catch (const std::exception& e) {
8✔
285
    if (g_logCommonErrors) {
×
286
      SLOG(g_log << Logger::Warning << "Error parsing a TCP query packet for edns subnet: " << e.what() << endl,
×
287
           g_slogtcpin->error(Logr::Warning, e.what(), "Error parsing a TCP query packet for edns subnet", "exception", Logging::Loggable("std::exception"), "remote", Logging::Loggable(conn->d_remote)));
×
288
    }
×
289
  }
×
290
}
8✔
291

292
static void doProcessTCPQuestion(std::unique_ptr<DNSComboWriter>& comboWriter, shared_ptr<TCPConnection>& conn, RunningTCPQuestionGuard& tcpGuard, int fileDesc)
293
{
852✔
294
  RecThreadInfo::self().incNumberOfDistributedQueries();
852✔
295
  struct timeval start{};
852✔
296
  Utility::gettimeofday(&start, nullptr);
852✔
297

298
  DNSName qname;
852✔
299
  uint16_t qtype = 0;
852✔
300
  uint16_t qclass = 0;
852✔
301
  bool needEDNSParse = false;
852✔
302
  string requestorId;
852✔
303
  string deviceId;
852✔
304
  string deviceName;
852✔
305
  bool logQuery = false;
852✔
306
  bool qnameParsed = false;
852✔
307
  boost::optional<uint32_t> ednsVersion;
852✔
308

309
  comboWriter->d_eventTrace.setEnabled(SyncRes::s_event_trace_enabled != 0);
852✔
310
  // eventTrace uses monotonic time, while OpenTelemetry uses absolute time. setEnabled()
311
  // established the reference point, get an absolute TS as close as possible to the
312
  // eventTrace start of trace time.
313
  auto traceTS = pdns::trace::timestamp();
852✔
314
  comboWriter->d_eventTrace.add(RecEventTrace::ReqRecv);
852✔
315
  if (SyncRes::eventTraceEnabled(SyncRes::event_trace_to_ot)) {
852!
316
    comboWriter->d_otTrace.clear();
×
317
    comboWriter->d_otTrace.start_time_unix_nano = traceTS;
×
318
  }
×
319
  auto luaconfsLocal = g_luaconfs.getLocal();
852✔
320
  if (checkProtobufExport(luaconfsLocal)) {
852✔
321
    needEDNSParse = true;
8✔
322
  }
8✔
323
  logQuery = t_protobufServers.servers && luaconfsLocal->protobufExportConfig.logQueries;
852✔
324
  comboWriter->d_logResponse = t_protobufServers.servers && luaconfsLocal->protobufExportConfig.logResponses;
852!
325

326
  if (needEDNSParse || (t_pdl && (t_pdl->hasGettagFFIFunc() || t_pdl->hasGettagFunc())) || comboWriter->d_mdp.d_header.opcode == static_cast<unsigned>(Opcode::Notify)) {
852✔
327

328
    try {
45✔
329
      EDNSOptionViewMap ednsOptions;
45✔
330
      comboWriter->d_ecsParsed = true;
45✔
331
      comboWriter->d_ecsFound = false;
45✔
332
      getQNameAndSubnet(conn->data, &qname, &qtype, &qclass,
45✔
333
                        comboWriter->d_ecsFound, &comboWriter->d_ednssubnet,
45✔
334
                        (g_gettagNeedsEDNSOptions || SyncRes::eventTraceEnabled(SyncRes::event_trace_to_ot)) ? &ednsOptions : nullptr, ednsVersion);
45!
335
      qnameParsed = true;
45✔
336

337
      if (SyncRes::eventTraceEnabled(SyncRes::event_trace_to_ot)) {
45!
338
        pdns::trace::extractOTraceIDs(ednsOptions, comboWriter->d_otTrace);
×
339
      }
×
340
      if (t_pdl) {
45✔
341
        try {
44✔
342
          if (t_pdl->hasGettagFFIFunc()) {
44✔
343
            RecursorLua4::FFIParams params(qname, qtype, comboWriter->d_local, comboWriter->d_remote, comboWriter->d_destination, comboWriter->d_source, comboWriter->d_ednssubnet.getSource(), comboWriter->d_data, comboWriter->d_gettagPolicyTags, comboWriter->d_records, ednsOptions, comboWriter->d_proxyProtocolValues, requestorId, deviceId, deviceName, comboWriter->d_routingTag, comboWriter->d_rcode, comboWriter->d_ttlCap, comboWriter->d_variable, true, logQuery, comboWriter->d_logResponse, comboWriter->d_followCNAMERecords, comboWriter->d_extendedErrorCode, comboWriter->d_extendedErrorExtra, comboWriter->d_responsePaddingDisabled, comboWriter->d_meta);
23✔
344
            auto match = comboWriter->d_eventTrace.add(RecEventTrace::LuaGetTagFFI);
23✔
345
            comboWriter->d_tag = t_pdl->gettag_ffi(params);
23✔
346
            comboWriter->d_eventTrace.add(RecEventTrace::LuaGetTagFFI, comboWriter->d_tag, false, match);
23✔
347
          }
23✔
348
          else if (t_pdl->hasGettagFunc()) {
21!
349
            auto match = comboWriter->d_eventTrace.add(RecEventTrace::LuaGetTag);
21✔
350
            comboWriter->d_tag = t_pdl->gettag(comboWriter->d_source, comboWriter->d_ednssubnet.getSource(), comboWriter->d_destination, qname, qtype, &comboWriter->d_gettagPolicyTags, comboWriter->d_data, ednsOptions, true, requestorId, deviceId, deviceName, comboWriter->d_routingTag, comboWriter->d_proxyProtocolValues);
21✔
351
            comboWriter->d_eventTrace.add(RecEventTrace::LuaGetTag, comboWriter->d_tag, false, match);
21✔
352
          }
21✔
353
          // Copy d_gettagPolicyTags to d_policyTags, so other Lua hooks see them and can add their
354
          // own. Before storing into the packetcache, the tags in d_gettagPolicyTags will be
355
          // cleared by addPolicyTagsToPBMessageIfNeeded() so they do *not* end up in the PC. When an
356
          // Protobuf message is constructed, one part comes from the PC (including the tags
357
          // set by non-gettag hooks), and the tags in d_gettagPolicyTags will be added by the code
358
          // constructing the PB message.
359
          comboWriter->d_policyTags = comboWriter->d_gettagPolicyTags;
44✔
360
        }
44✔
361
        catch (const MOADNSException& moadnsexception) {
44✔
362
          if (g_logCommonErrors) {
×
363
            g_slogtcpin->error(moadnsexception.what(), "Error parsing a query packet for tag determination", "qname", Logging::Loggable(qname), "excepion", Logging::Loggable("MOADNSException"));
×
364
          }
×
365
        }
×
366
        catch (const std::exception& stdException) {
44✔
367
          g_rateLimitedLogger.log(g_slogtcpin, "Error parsing a query packet for tag determination", stdException, "qname", Logging::Loggable(qname), "remote", Logging::Loggable(conn->d_remote));
×
368
        }
×
369
      }
44✔
370
    }
45✔
371
    catch (const std::exception& stdException) {
45✔
372
      g_rateLimitedLogger.log(g_slogudpin, "Error parsing a query packet for tag determination, setting tag=0", stdException, "remote", Logging::Loggable(conn->d_remote));
×
373
    }
×
374
  }
45✔
375

376
  if (comboWriter->d_tag == 0 && !comboWriter->d_responsePaddingDisabled && g_paddingFrom.match(comboWriter->d_remote)) {
852!
377
    comboWriter->d_tag = g_paddingTag;
×
378
  }
×
379

380
  const dnsheader_aligned headerdata(conn->data.data());
852✔
381
  const struct dnsheader* dnsheader = headerdata.get();
852✔
382

383
  if (t_protobufServers.servers || t_outgoingProtobufServers.servers) {
852!
384
    comboWriter->d_requestorId = std::move(requestorId);
8✔
385
    comboWriter->d_deviceId = std::move(deviceId);
8✔
386
    comboWriter->d_deviceName = std::move(deviceName);
8✔
387
    comboWriter->d_uuid = getUniqueID();
8✔
388
  }
8✔
389

390
  if (t_protobufServers.servers) {
852✔
391
    doProtobufLogQuery(logQuery, luaconfsLocal, comboWriter, qname, qtype, qclass, dnsheader, conn, ednsVersion);
8✔
392
  }
8✔
393

394
  if (t_pdl) {
852✔
395
    bool ipf = t_pdl->ipfilter(comboWriter->d_source, comboWriter->d_destination, *dnsheader, comboWriter->d_eventTrace);
195✔
396
    if (ipf) {
195✔
397
      if (!g_quiet) {
2!
398
        SLOG(g_log << Logger::Notice << RecThreadInfo::id() << " [" << g_multiTasker->getTid() << "/" << g_multiTasker->numProcesses() << "] DROPPED TCP question from " << comboWriter->d_source.toStringWithPort() << (comboWriter->d_source != comboWriter->d_remote ? " (via " + comboWriter->d_remote.toStringWithPort() + ")" : "") << " based on policy" << endl,
2✔
399
             g_slogtcpin->info(Logr::Info, "Dropped TCP question based on policy", "remote", Logging::Loggable(conn->d_remote), "source", Logging::Loggable(comboWriter->d_source)));
2✔
400
      }
2✔
401
      t_Counters.at(rec::Counter::policyDrops)++;
2✔
402
      return;
2✔
403
    }
2✔
404
  }
195✔
405

406
  if (comboWriter->d_mdp.d_header.qr) {
850!
407
    t_Counters.at(rec::Counter::ignoredCount)++;
×
408
    if (g_logCommonErrors) {
×
409
      SLOG(g_log << Logger::Error << "Ignoring answer from TCP client " << comboWriter->getRemote() << " on server socket!" << endl,
×
410
           g_slogtcpin->info(Logr::Error, "Ignoring answer from TCP client on server socket", "remote", Logging::Loggable(comboWriter->getRemote())));
×
411
    }
×
412
    return;
×
413
  }
×
414
  if (comboWriter->d_mdp.d_header.opcode != static_cast<unsigned>(Opcode::Query) && comboWriter->d_mdp.d_header.opcode != static_cast<unsigned>(Opcode::Notify)) {
850!
415
    t_Counters.at(rec::Counter::ignoredCount)++;
×
416
    if (g_logCommonErrors) {
×
417
      SLOG(g_log << Logger::Error << "Ignoring unsupported opcode " << Opcode::to_s(comboWriter->d_mdp.d_header.opcode) << " from TCP client " << comboWriter->getRemote() << " on server socket!" << endl,
×
418
           g_slogtcpin->info(Logr::Error, "Ignoring unsupported opcode from TCP client", "remote", Logging::Loggable(comboWriter->getRemote()), "opcode", Logging::Loggable(Opcode::to_s(comboWriter->d_mdp.d_header.opcode))));
×
419
    }
×
420
    sendErrorOverTCP(comboWriter, RCode::NotImp);
×
421
    tcpGuard.keep();
×
422
    return;
×
423
  }
×
424
  if (dnsheader->qdcount == 0U) {
850!
425
    t_Counters.at(rec::Counter::emptyQueriesCount)++;
×
426
    if (g_logCommonErrors) {
×
427
      SLOG(g_log << Logger::Error << "Ignoring empty (qdcount == 0) query from " << comboWriter->getRemote() << " on server socket!" << endl,
×
428
           g_slogtcpin->info(Logr::Error, "Ignoring empty (qdcount == 0) query on server socket", "remote", Logging::Loggable(comboWriter->getRemote())));
×
429
    }
×
430
    sendErrorOverTCP(comboWriter, RCode::NotImp);
×
431
    tcpGuard.keep();
×
432
    return;
×
433
  }
×
434
  {
850✔
435
    // We have read a proper query
436
    ++t_Counters.at(rec::Counter::qcounter);
850✔
437
    ++t_Counters.at(rec::Counter::tcpqcounter);
850✔
438
    if (comboWriter->d_source.sin4.sin_family == AF_INET6) {
850✔
439
      ++t_Counters.at(rec::Counter::ipv6qcounter);
16✔
440
    }
16✔
441

442
    if (comboWriter->d_mdp.d_header.opcode == static_cast<unsigned>(Opcode::Notify)) {
850✔
443
      handleNotify(comboWriter, qname);
1✔
444
    }
1✔
445

446
    string response;
850✔
447
    RecursorPacketCache::OptPBData pbData{boost::none};
850✔
448

449
    if (comboWriter->d_mdp.d_header.opcode == static_cast<unsigned>(Opcode::Query)) {
850✔
450
      /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable,
451
         but it means that the hash would not be computed. If some script decides at a later time to mark back the answer
452
         as cacheable we would cache it with a wrong tag, so better safe than sorry. */
453
      auto match = comboWriter->d_eventTrace.add(RecEventTrace::PCacheCheck);
849✔
454
      bool cacheHit = checkForCacheHit(qnameParsed, comboWriter->d_tag, conn->data, qname, qtype, qclass, g_now, response, comboWriter->d_qhash, pbData, true, comboWriter->d_source, comboWriter->d_mappedSource);
849✔
455
      comboWriter->d_eventTrace.add(RecEventTrace::PCacheCheck, cacheHit, false, match);
849✔
456

457
      if (cacheHit) {
849✔
458
        if (!g_quiet) {
66✔
459
          SLOG(g_log << Logger::Notice << RecThreadInfo::id() << " TCP question answered from packet cache tag=" << comboWriter->d_tag << " from " << comboWriter->d_source.toStringWithPort() << (comboWriter->d_source != comboWriter->d_remote ? " (via " + comboWriter->d_remote.toStringWithPort() + ")" : "") << endl,
40✔
460
               g_slogtcpin->info(Logr::Notice, "TCP question answered from packet cache", "tag", Logging::Loggable(comboWriter->d_tag),
40✔
461
                                 "qname", Logging::Loggable(qname), "qtype", Logging::Loggable(QType(qtype)),
40✔
462
                                 "source", Logging::Loggable(comboWriter->d_source), "remote", Logging::Loggable(comboWriter->d_remote)));
40✔
463
        }
40✔
464

465
        bool hadError = sendResponseOverTCP(comboWriter, response);
66✔
466
        finishTCPReply(comboWriter, hadError, false);
66✔
467
        struct timeval now{};
66✔
468
        Utility::gettimeofday(&now, nullptr);
66✔
469
        uint64_t spentUsec = uSec(now - start);
66✔
470
        t_Counters.at(rec::Histogram::cumulativeAnswers)(spentUsec);
66✔
471
        comboWriter->d_eventTrace.add(RecEventTrace::AnswerSent);
66✔
472

473
        if (t_protobufServers.servers && comboWriter->d_logResponse && (!luaconfsLocal->protobufExportConfig.taggedOnly || (pbData && pbData->d_tagged))) {
66!
474
          struct timeval tval{
3✔
475
            0, 0};
3✔
476
          protobufLogResponse(qname, qtype, dnsheader, luaconfsLocal, pbData, tval, true, comboWriter->d_source, comboWriter->d_destination, comboWriter->d_mappedSource, comboWriter->d_ednssubnet, comboWriter->d_uuid, comboWriter->d_requestorId, comboWriter->d_deviceId, comboWriter->d_deviceName, comboWriter->d_meta, comboWriter->d_eventTrace, comboWriter->d_otTrace, comboWriter->d_policyTags);
3✔
477
        }
3✔
478

479
        if (comboWriter->d_eventTrace.enabled() && SyncRes::eventTraceEnabled(SyncRes::event_trace_to_log)) {
66!
480
          SLOG(g_log << Logger::Info << comboWriter->d_eventTrace.toString() << endl,
×
481
               g_slogtcpin->info(Logr::Info, comboWriter->d_eventTrace.toString())); // More fancy?
×
482
        }
×
483
        tcpGuard.keep();
66✔
484
        t_Counters.updateSnap(g_regressionTestMode);
66✔
485
        return;
66✔
486
      } // cache hit
66✔
487
    } // query opcode
849✔
488

489
    if (comboWriter->d_mdp.d_header.opcode == static_cast<unsigned>(Opcode::Notify)) {
784✔
490
      if (!g_quiet) {
1!
491
        SLOG(g_log << Logger::Notice << RecThreadInfo::id() << " got NOTIFY for " << qname.toLogString() << " from " << comboWriter->d_source.toStringWithPort() << (comboWriter->d_source != comboWriter->d_remote ? " (via " + comboWriter->d_remote.toStringWithPort() + ")" : "") << endl,
1✔
492
             g_slogtcpin->info(Logr::Notice, "Got NOTIFY", "qname", Logging::Loggable(qname), "source", Logging::Loggable(comboWriter->d_source), "remote", Logging::Loggable(comboWriter->d_remote)));
1✔
493
      }
1✔
494

495
      requestWipeCaches(qname);
1✔
496

497
      // the operation will now be treated as a Query, generating
498
      // a normal response, as the rest of the code does not
499
      // check dh->opcode, but we need to ensure that the response
500
      // to this request does not get put into the packet cache
501
      comboWriter->d_variable = true;
1✔
502
    }
1✔
503

504
    // setup for startDoResolve() in an mthread
505
    ++conn->d_requestsInFlight;
784✔
506
    if (conn->d_requestsInFlight >= TCPConnection::s_maxInFlight) {
784✔
507
      t_fdm->removeReadFD(fileDesc); // should no longer awake ourselves when there is data to read
2✔
508
    }
2✔
509
    else {
782✔
510
      Utility::gettimeofday(&g_now, nullptr); // needed?
782✔
511
      struct timeval ttd = g_now;
782✔
512
      t_fdm->setReadTTD(fileDesc, ttd, g_tcpTimeout);
782✔
513
    }
782✔
514
    tcpGuard.keep();
784✔
515
    g_multiTasker->makeThread(startDoResolve, comboWriter.release()); // deletes dc
784✔
516
  } // good query
784✔
517
}
784✔
518

519
static void handleRunningTCPQuestion(int fileDesc, FDMultiplexer::funcparam_t& var) // NOLINT(readability-function-cognitive-complexity)
520
{
1,565✔
521
  auto conn = boost::any_cast<shared_ptr<TCPConnection>>(var);
1,565✔
522

523
  RunningTCPQuestionGuard tcpGuard{fileDesc};
1,565✔
524

525
  if (conn->state == TCPConnection::PROXYPROTOCOLHEADER) {
1,565✔
526
    ssize_t bytes = recv(conn->getFD(), &conn->data.at(conn->proxyProtocolGot), conn->proxyProtocolNeed, 0);
131✔
527
    if (bytes <= 0) {
131✔
528
      tcpGuard.handleTCPReadResult(fileDesc, bytes);
7✔
529
      return;
7✔
530
    }
7✔
531

532
    conn->proxyProtocolGot += bytes;
124✔
533
    conn->data.resize(conn->proxyProtocolGot);
124✔
534
    ssize_t remaining = isProxyHeaderComplete(conn->data);
124✔
535
    if (remaining == 0) {
124✔
536
      if (g_logCommonErrors) {
4!
537
        SLOG(g_log << Logger::Error << "Unable to consume proxy protocol header in packet from TCP client " << conn->d_remote.toStringWithPort() << endl,
4✔
538
             g_slogtcpin->info(Logr::Error, "Unable to consume proxy protocol header in packet from TCP client", "remote", Logging::Loggable(conn->d_remote)));
4✔
539
      }
4✔
540
      ++t_Counters.at(rec::Counter::proxyProtocolInvalidCount);
4✔
541
      return;
4✔
542
    }
4✔
543
    if (remaining < 0) {
120✔
544
      conn->proxyProtocolNeed = -remaining;
97✔
545
      conn->data.resize(conn->proxyProtocolGot + conn->proxyProtocolNeed);
97✔
546
      tcpGuard.keep();
97✔
547
      return;
97✔
548
    }
97✔
549
    {
23✔
550
      /* proxy header received */
551
      /* we ignore the TCP field for now, but we could properly set whether
552
         the connection was received over UDP or TCP if needed */
553
      bool tcp = false;
23✔
554
      bool proxy = false;
23✔
555
      size_t used = parseProxyHeader(conn->data, proxy, conn->d_source, conn->d_destination, tcp, conn->proxyProtocolValues);
23✔
556
      if (used <= 0) {
23!
557
        if (g_logCommonErrors) {
×
558
          SLOG(g_log << Logger::Error << "Unable to parse proxy protocol header in packet from TCP client " << conn->d_remote.toStringWithPort() << endl,
×
559
               g_slogtcpin->info(Logr::Error, "Unable to parse proxy protocol header in packet from TCP client", "remote", Logging::Loggable(conn->d_remote)));
×
560
        }
×
561
        ++t_Counters.at(rec::Counter::proxyProtocolInvalidCount);
×
562
        return;
×
563
      }
×
564
      if (static_cast<size_t>(used) > g_proxyProtocolMaximumSize) {
23✔
565
        if (g_logCommonErrors) {
2!
566
          SLOG(g_log << Logger::Error << "Proxy protocol header in packet from TCP client " << conn->d_remote.toStringWithPort() << " is larger than proxy-protocol-maximum-size (" << used << "), dropping" << endl,
2✔
567
               g_slogtcpin->info(Logr::Error, "Proxy protocol header in packet from TCP client is larger than proxy-protocol-maximum-size", "remote", Logging::Loggable(conn->d_remote), "size", Logging::Loggable(used)));
2✔
568
        }
2✔
569
        ++t_Counters.at(rec::Counter::proxyProtocolInvalidCount);
2✔
570
        return;
2✔
571
      }
2✔
572

573
      /* Now that we have retrieved the address of the client, as advertised by the proxy
574
         via the proxy protocol header, check that it is allowed by our ACL */
575
      /* note that if the proxy header used a 'LOCAL' command, the original source and destination are untouched so everything should be fine */
576
      conn->d_mappedSource = conn->d_source;
21✔
577
      if (t_proxyMapping) {
21!
578
        if (const auto* iter = t_proxyMapping->lookup(conn->d_source)) {
×
579
          conn->d_mappedSource = iter->second.address;
×
580
          ++iter->second.stats.netmaskMatches;
×
581
        }
×
582
      }
×
583
      if (t_remotes) {
21!
584
        t_remotes->push_back(conn->d_source);
21✔
585
      }
21✔
586
      if (t_allowFrom && !t_allowFrom->match(&conn->d_mappedSource)) {
21!
587
        if (!g_quiet) {
4!
588
          SLOG(g_log << Logger::Error << "[" << g_multiTasker->getTid() << "] dropping TCP query from " << conn->d_mappedSource.toString() << ", address not matched by allow-from" << endl,
4✔
589
               g_slogtcpin->info(Logr::Error, "Dropping TCP query, address not matched by allow-from", "remote", Logging::Loggable(conn->d_remote)));
4✔
590
        }
4✔
591

592
        ++t_Counters.at(rec::Counter::unauthorizedTCP);
4✔
593
        return;
4✔
594
      }
4✔
595

596
      conn->data.resize(2);
17✔
597
      conn->state = TCPConnection::BYTE0;
17✔
598
    }
17✔
599
  }
17✔
600

601
  if (conn->state == TCPConnection::BYTE0) {
1,451✔
602
    ssize_t bytes = recv(conn->getFD(), conn->data.data(), 2, 0);
1,239✔
603
    if (bytes == 1) {
1,239✔
604
      conn->state = TCPConnection::BYTE1;
2✔
605
    }
2✔
606
    if (bytes == 2) {
1,239✔
607
      conn->qlen = (((unsigned char)conn->data[0]) << 8) + (unsigned char)conn->data[1];
853✔
608
      conn->data.resize(conn->qlen);
853✔
609
      conn->bytesread = 0;
853✔
610
      conn->state = TCPConnection::GETQUESTION;
853✔
611
    }
853✔
612
    if (bytes <= 0) {
1,239✔
613
      tcpGuard.handleTCPReadResult(fileDesc, bytes);
384✔
614
      return;
384✔
615
    }
384✔
616
  }
1,239✔
617

618
  if (conn->state == TCPConnection::BYTE1) {
1,067✔
619
    ssize_t bytes = recv(conn->getFD(), &conn->data[1], 1, 0);
4✔
620
    if (bytes == 1) {
4✔
621
      conn->state = TCPConnection::GETQUESTION;
2✔
622
      conn->qlen = (((unsigned char)conn->data[0]) << 8) + (unsigned char)conn->data[1];
2✔
623
      conn->data.resize(conn->qlen);
2✔
624
      conn->bytesread = 0;
2✔
625
    }
2✔
626
    if (bytes <= 0) {
4✔
627
      if (!tcpGuard.handleTCPReadResult(fileDesc, bytes)) {
2!
628
        if (g_logCommonErrors) {
×
629
          SLOG(g_log << Logger::Error << "TCP client " << conn->d_remote.toStringWithPort() << " disconnected after first byte" << endl,
×
630
               g_slogtcpin->info(Logr::Error, "TCP client disconnected after first byte", "remote", Logging::Loggable(conn->d_remote)));
×
631
        }
×
632
      }
×
633
      return;
2✔
634
    }
2✔
635
  }
4✔
636

637
  if (conn->state == TCPConnection::GETQUESTION) {
1,065!
638
    ssize_t bytes = recv(conn->getFD(), &conn->data[conn->bytesread], conn->qlen - conn->bytesread, 0);
1,065✔
639
    if (bytes <= 0) {
1,065✔
640
      if (!tcpGuard.handleTCPReadResult(fileDesc, bytes)) {
24✔
641
        if (g_logCommonErrors) {
3!
642
          SLOG(g_log << Logger::Error << "TCP client " << conn->d_remote.toStringWithPort() << " disconnected while reading question body" << endl,
3✔
643
               g_slogtcpin->info(Logr::Error, "TCP client disconnected while reading question body", "remote", Logging::Loggable(conn->d_remote)));
3✔
644
        }
3✔
645
      }
3✔
646
      return;
24✔
647
    }
24✔
648
    if (bytes > std::numeric_limits<std::uint16_t>::max()) {
1,041!
649
      if (g_logCommonErrors) {
×
650
        SLOG(g_log << Logger::Error << "TCP client " << conn->d_remote.toStringWithPort() << " sent an invalid question size while reading question body" << endl,
×
651
             g_slogtcpin->info(Logr::Error, "TCP client sent an invalid question size while reading question body", "remote", Logging::Loggable(conn->d_remote)));
×
652
      }
×
653
      return;
×
654
    }
×
655
    conn->bytesread += (uint16_t)bytes;
1,041✔
656
    if (conn->bytesread == conn->qlen) {
1,041✔
657
      conn->state = TCPConnection::BYTE0;
852✔
658
      std::unique_ptr<DNSComboWriter> comboWriter;
852✔
659
      try {
852✔
660
        comboWriter = std::make_unique<DNSComboWriter>(conn->data, g_now, t_pdl);
852✔
661
      }
852✔
662
      catch (const MOADNSException& mde) {
852✔
663
        t_Counters.at(rec::Counter::clientParseError)++;
×
664
        if (g_logCommonErrors) {
×
665
          SLOG(g_log << Logger::Error << "Unable to parse packet from TCP client " << conn->d_remote.toStringWithPort() << endl,
×
666
               g_slogtcpin->info(Logr::Error, "Unable to parse packet from TCP client", "remte", Logging::Loggable(conn->d_remote)));
×
667
        }
×
668
        return;
×
669
      }
×
670

671
      comboWriter->d_tcpConnection = conn; // carry the torch
852✔
672
      comboWriter->setSocket(conn->getFD()); // this is the only time a copy is made of the actual fd
852✔
673
      comboWriter->d_tcp = true;
852✔
674
      comboWriter->setRemote(conn->d_remote); // the address the query was received from
852✔
675
      comboWriter->setSource(conn->d_source); // the address we assume the query is coming from, might be set by proxy protocol
852✔
676
      ComboAddress dest;
852✔
677
      dest.reset();
852✔
678
      dest.sin4.sin_family = conn->d_remote.sin4.sin_family;
852✔
679
      socklen_t len = dest.getSocklen();
852✔
680
      getsockname(conn->getFD(), reinterpret_cast<sockaddr*>(&dest), &len); // if this fails, we're ok with it NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
852✔
681
      comboWriter->setLocal(dest); // the address we received the query on
852✔
682
      comboWriter->setDestination(conn->d_destination); // the address we assume the query is received on, might be set by proxy protocol
852✔
683
      comboWriter->setMappedSource(conn->d_mappedSource); // the address we assume the query is coming from after table based mapping
852✔
684
      /* we can't move this if we want to be able to access the values in
685
         all queries sent over this connection */
686
      comboWriter->d_proxyProtocolValues = conn->proxyProtocolValues;
852✔
687

688
      doProcessTCPQuestion(comboWriter, conn, tcpGuard, fileDesc);
852✔
689
    } // reading query
852✔
690
  }
1,041✔
691
  // more to come
692
  tcpGuard.keep();
1,041✔
693
}
1,041✔
694

695
//! Handle new incoming TCP connection
696
void handleNewTCPQuestion(int fileDesc, [[maybe_unused]] FDMultiplexer::funcparam_t& var)
697
{
404✔
698
  ComboAddress addr;
404✔
699
  socklen_t addrlen = sizeof(addr);
404✔
700
  int newsock = accept(fileDesc, reinterpret_cast<struct sockaddr*>(&addr), &addrlen); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
404✔
701
  if (newsock < 0) {
404!
702
    return;
×
703
  }
×
704
  auto closeSock = [newsock](rec::Counter cnt, const string& msg) {
404✔
705
    try {
×
706
      closesocket(newsock);
×
707
      t_Counters.at(cnt)++;
×
708
      // We want this bump to percolate up without too much delay
709
      t_Counters.updateSnap(false);
×
710
    }
×
711
    catch (const PDNSException& e) {
×
712
      g_slogtcpin->error(Logr::Error, e.reason, msg, "exception", Logging::Loggable("PDNSException"));
×
713
    }
×
714
  };
×
715

716
  if (TCPConnection::getCurrentConnections() >= g_maxTCPClients) {
404!
717
    closeSock(rec::Counter::tcpOverflow, "Error closing TCP socket after an overflow drop");
×
718
    return;
×
719
  }
×
720
  if (g_multiTasker->numProcesses() >= g_maxMThreads) {
404!
721
    closeSock(rec::Counter::overCapacityDrops, "Error closing TCP socket after an over capacity drop");
×
722
    return;
×
723
  }
×
724

725
  ComboAddress destaddr;
404✔
726
  socklen_t len = sizeof(destaddr);
404✔
727
  getsockname(newsock, reinterpret_cast<sockaddr*>(&destaddr), &len); // if this fails, we're ok with it NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
404✔
728
  bool fromProxyProtocolSource = expectProxyProtocol(addr, destaddr);
404✔
729
  if (!fromProxyProtocolSource && t_remotes) {
404!
730
    t_remotes->push_back(addr);
370✔
731
  }
370✔
732
  ComboAddress mappedSource = addr;
404✔
733
  if (!fromProxyProtocolSource && t_proxyMapping) {
404✔
734
    if (const auto* iter = t_proxyMapping->lookup(addr)) {
5!
735
      mappedSource = iter->second.address;
5✔
736
      ++iter->second.stats.netmaskMatches;
5✔
737
    }
5✔
738
  }
5✔
739
  if (!fromProxyProtocolSource && t_allowFrom && !t_allowFrom->match(&mappedSource)) {
404!
740
    if (!g_quiet) {
×
741
      SLOG(g_log << Logger::Error << "[" << g_multiTasker->getTid() << "] dropping TCP query from " << mappedSource.toString() << ", address neither matched by allow-from nor proxy-protocol-from" << endl,
×
742
           g_slogtcpin->info(Logr::Error, "dropping TCP query address neither matched by allow-from nor proxy-protocol-from", "source", Logging::Loggable(mappedSource)));
×
743
    }
×
744
    closeSock(rec::Counter::unauthorizedTCP, "Error closing TCP socket after an ACL drop");
×
745
    return;
×
746
  }
×
747

748
  if (g_maxTCPPerClient > 0 && t_tcpClientCounts->count(addr) > 0 && (*t_tcpClientCounts)[addr] >= g_maxTCPPerClient) {
404!
749
    closeSock(rec::Counter::tcpClientOverflow, "Error closing TCP socket after a client overflow drop");
×
750
    return;
×
751
  }
×
752

753
  setNonBlocking(newsock);
404✔
754
  setTCPNoDelay(newsock);
404✔
755
  std::shared_ptr<TCPConnection> tcpConn = std::make_shared<TCPConnection>(newsock, addr);
404✔
756
  tcpConn->d_source = addr;
404✔
757
  tcpConn->d_destination = destaddr;
404✔
758
  tcpConn->d_mappedSource = mappedSource;
404✔
759

760
  if (fromProxyProtocolSource) {
404✔
761
    tcpConn->proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
34✔
762
    tcpConn->data.resize(tcpConn->proxyProtocolNeed);
34✔
763
    tcpConn->state = TCPConnection::PROXYPROTOCOLHEADER;
34✔
764
  }
34✔
765
  else {
370✔
766
    tcpConn->state = TCPConnection::BYTE0;
370✔
767
  }
370✔
768

769
  timeval ttd{};
404✔
770
  Utility::gettimeofday(&ttd, nullptr);
404✔
771
  ttd.tv_sec += g_tcpTimeout;
404✔
772

773
  t_fdm->addReadFD(tcpConn->getFD(), handleRunningTCPQuestion, tcpConn, &ttd);
404✔
774
}
404✔
775

776
static void TCPIOHandlerIO(int fileDesc, FDMultiplexer::funcparam_t& var);
777

778
static void TCPIOHandlerStateChange(IOState oldstate, IOState newstate, std::shared_ptr<PacketID>& pid)
779
{
66✔
780
  TCPLOG(pid->tcpsock, "State transation " << int(oldstate) << "->" << int(newstate) << endl);
66✔
781

782
  pid->lowState = newstate;
66✔
783

784
  // handle state transitions
785
  switch (oldstate) {
66!
786
  case IOState::NeedRead:
26✔
787

788
    switch (newstate) {
26!
789
    case IOState::NeedWrite:
×
790
      TCPLOG(pid->tcpsock, "NeedRead -> NeedWrite: flip FD" << endl);
×
791
      t_fdm->alterFDToWrite(pid->tcpsock, TCPIOHandlerIO, pid);
×
792
      break;
×
793
    case IOState::NeedRead:
3✔
794
      break;
3✔
795
    case IOState::Done:
23✔
796
      TCPLOG(pid->tcpsock, "Done -> removeReadFD" << endl);
23✔
797
      t_fdm->removeReadFD(pid->tcpsock);
23✔
798
      break;
23✔
799
    case IOState::Async:
×
800
      throw std::runtime_error("TLS async mode not supported");
×
801
      break;
×
802
    }
26✔
803
    break;
26✔
804

805
  case IOState::NeedWrite:
26✔
806

807
    switch (newstate) {
11!
808
    case IOState::NeedRead:
5✔
809
      TCPLOG(pid->tcpsock, "NeedWrite -> NeedRead: flip FD" << endl);
5✔
810
      t_fdm->alterFDToRead(pid->tcpsock, TCPIOHandlerIO, pid);
5✔
811
      break;
5✔
812
    case IOState::NeedWrite:
×
813
      break;
×
814
    case IOState::Done:
6✔
815
      TCPLOG(pid->tcpsock, "Done -> removeWriteFD" << endl);
6✔
816
      t_fdm->removeWriteFD(pid->tcpsock);
6✔
817
      break;
6✔
818
    case IOState::Async:
×
819
      throw std::runtime_error("TLS async mode not supported");
×
820
      break;
×
821
    }
11✔
822
    break;
11✔
823

824
  case IOState::Done:
29✔
825
    switch (newstate) {
29!
826
    case IOState::NeedRead:
18✔
827
      TCPLOG(pid->tcpsock, "NeedRead: addReadFD" << endl);
18✔
828
      t_fdm->addReadFD(pid->tcpsock, TCPIOHandlerIO, pid);
18✔
829
      break;
18✔
830
    case IOState::NeedWrite:
11✔
831
      TCPLOG(pid->tcpsock, "NeedWrite: addWriteFD" << endl);
11✔
832
      t_fdm->addWriteFD(pid->tcpsock, TCPIOHandlerIO, pid);
11✔
833
      break;
11✔
834
    case IOState::Done:
×
835
      break;
×
836
    case IOState::Async:
×
837
      throw std::runtime_error("TLS async mode not supported");
×
838
      break;
×
839
    }
29✔
840
    break;
29✔
841

842
  case IOState::Async:
29!
843
    throw std::runtime_error("TLS async mode not supported");
×
844
    break;
×
845
  }
66✔
846
}
66✔
847

848
static void TCPIOHandlerIO(int fileDesc, FDMultiplexer::funcparam_t& var)
849
{
37✔
850
  auto pid = boost::any_cast<std::shared_ptr<PacketID>>(var);
37✔
851
  assert(pid->tcphandler); // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay): def off assert triggers it
37✔
852
  assert(fileDesc == pid->tcphandler->getDescriptor()); // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay) idem
×
853
  IOState newstate = IOState::Done;
×
854

855
  TCPLOG(pid->tcpsock, "TCPIOHandlerIO: lowState " << int(pid->lowState) << endl);
37✔
856

857
  // In the code below, we want to update the state of the fd before calling sendEvent
858
  // a sendEvent might close the fd, and some poll multiplexers do not like to manipulate a closed fd
859

860
  switch (pid->highState) {
37!
861
  case TCPAction::DoingRead:
21✔
862
    TCPLOG(pid->tcpsock, "highState: Reading" << endl);
21✔
863
    // In arecvtcp, the buffer was resized already so inWanted bytes will fit
864
    // try reading
865
    try {
21✔
866
      newstate = pid->tcphandler->tryRead(pid->inMSG, pid->inPos, pid->inWanted);
21✔
867
      switch (newstate) {
21!
868
      case IOState::Done:
18✔
869
      case IOState::NeedRead:
21✔
870
        TCPLOG(pid->tcpsock, "tryRead: Done or NeedRead " << int(newstate) << ' ' << pid->inPos << '/' << pid->inWanted << endl);
21✔
871
        TCPLOG(pid->tcpsock, "TCPIOHandlerIO " << pid->inWanted << ' ' << pid->inIncompleteOkay << endl);
21✔
872
        if (pid->inPos == pid->inWanted || (pid->inIncompleteOkay && pid->inPos > 0)) {
21!
873
          pid->inMSG.resize(pid->inPos); // old content (if there) + new bytes read, only relevant for the inIncompleteOkay case
18✔
874
          newstate = IOState::Done;
18✔
875
          TCPIOHandlerStateChange(pid->lowState, newstate, pid);
18✔
876
          g_multiTasker->sendEvent(pid, &pid->inMSG);
18✔
877
          return;
18✔
878
        }
18✔
879
        break;
3✔
880
      case IOState::NeedWrite:
3!
881
        break;
×
882
      case IOState::Async:
×
883
        throw std::runtime_error("TLS async mode not supported");
×
884
        break;
×
885
      }
21✔
886
    }
21✔
887
    catch (const std::exception& e) {
21✔
888
      newstate = IOState::Done;
×
889
      TCPLOG(pid->tcpsock, "read exception..." << e.what() << endl);
×
890
      PacketBuffer empty;
×
891
      TCPIOHandlerStateChange(pid->lowState, newstate, pid);
×
892
      g_multiTasker->sendEvent(pid, &empty); // this conveys error status
×
893
      return;
×
894
    }
×
895
    break;
3✔
896

897
  case TCPAction::DoingWrite:
16✔
898
    TCPLOG(pid->tcpsock, "highState: Writing" << endl);
16✔
899
    try {
16✔
900
      TCPLOG(pid->tcpsock, "tryWrite: " << pid->outPos << '/' << pid->outMSG.size() << ' ' << " -> ");
16✔
901
      newstate = pid->tcphandler->tryWrite(pid->outMSG, pid->outPos, pid->outMSG.size());
16✔
902
      TCPLOG(pid->tcpsock, pid->outPos << '/' << pid->outMSG.size() << endl);
16✔
903
      switch (newstate) {
16!
904
      case IOState::Done: {
11✔
905
        TCPLOG(pid->tcpsock, "tryWrite: Done" << endl);
11✔
906
        TCPIOHandlerStateChange(pid->lowState, newstate, pid);
11✔
907
        g_multiTasker->sendEvent(pid, &pid->outMSG); // send back what we sent to convey everything is ok
11✔
908
        return;
11✔
909
      }
×
910
      case IOState::NeedRead:
5✔
911
        TCPLOG(pid->tcpsock, "tryWrite: NeedRead" << endl);
5✔
912
        break;
5✔
913
      case IOState::NeedWrite:
×
914
        TCPLOG(pid->tcpsock, "tryWrite: NeedWrite" << endl);
×
915
        break;
×
916
      case IOState::Async:
×
917
        throw std::runtime_error("TLS async mode not supported");
×
918
        break;
×
919
      }
16✔
920
    }
16✔
921
    catch (const std::exception& e) {
16✔
922
      newstate = IOState::Done;
×
923
      TCPLOG(pid->tcpsock, "write exception..." << e.what() << endl);
×
924
      PacketBuffer sent;
×
925
      TCPIOHandlerStateChange(pid->lowState, newstate, pid);
×
926
      g_multiTasker->sendEvent(pid, &sent); // we convey error status by sending empty string
×
927
      return;
×
928
    }
×
929
    break;
5✔
930
  }
37✔
931

932
  // Cases that did not end up doing a sendEvent
933
  TCPIOHandlerStateChange(pid->lowState, newstate, pid);
8✔
934
}
8✔
935

936
void checkFastOpenSysctl([[maybe_unused]] bool active, [[maybe_unused]] Logr::log_t log)
937
{
×
938
#ifdef __linux__
×
939
  string line;
×
940
  if (readFileIfThere("/proc/sys/net/ipv4/tcp_fastopen", &line)) {
×
941
    int flag = std::stoi(line);
×
942
    if (active && !(flag & 1)) {
×
943
      SLOG(g_log << Logger::Error << "tcp-fast-open-connect enabled but net.ipv4.tcp_fastopen does not allow it" << endl,
×
944
           log->info(Logr::Error, "tcp-fast-open-connect enabled but net.ipv4.tcp_fastopen does not allow it"));
×
945
    }
×
946
    if (!active && !(flag & 2)) {
×
947
      SLOG(g_log << Logger::Error << "tcp-fast-open enabled but net.ipv4.tcp_fastopen does not allow it" << endl,
×
948
           log->info(Logr::Error, "tcp-fast-open enabled but net.ipv4.tcp_fastopen does not allow it"));
×
949
    }
×
950
  }
×
951
  else {
×
952
    SLOG(g_log << Logger::Notice << "Cannot determine if kernel settings allow fast-open" << endl,
×
953
         log->info(Logr::Notice, "Cannot determine if kernel settings allow fast-open"));
×
954
  }
×
955
#else
956
  SLOG(g_log << Logger::Notice << "Cannot determine if kernel settings allow fast-open" << endl,
957
       log->info(Logr::Notice, "Cannot determine if kernel settings allow fast-open"));
958
#endif
959
}
×
960

961
void checkTFOconnect(Logr::log_t log)
962
{
×
963
  try {
×
964
    Socket socket(AF_INET, SOCK_STREAM);
×
965
    socket.setNonBlocking();
×
966
    socket.setFastOpenConnect();
×
967
  }
×
968
  catch (const NetworkError& e) {
×
969
    SLOG(g_log << Logger::Error << "tcp-fast-open-connect enabled but returned error: " << e.what() << endl,
×
970
         log->error(Logr::Error, e.what(), "tcp-fast-open-connect enabled but returned error"));
×
971
  }
×
972
}
×
973

974
LWResult::Result asendtcp(const PacketBuffer& data, shared_ptr<TCPIOHandler>& handler)
975
{
24✔
976
  TCPLOG(handler->getDescriptor(), "asendtcp called " << data.size() << endl);
24✔
977

978
  auto pident = std::make_shared<PacketID>();
24✔
979
  pident->tcphandler = handler;
24✔
980
  pident->tcpsock = handler->getDescriptor();
24✔
981
  pident->outMSG = data;
24✔
982
  pident->highState = TCPAction::DoingWrite;
24✔
983

984
  IOState state = IOState::Done;
24✔
985
  try {
24✔
986
    TCPLOG(pident->tcpsock, "Initial tryWrite: " << pident->outPos << '/' << pident->outMSG.size() << ' ' << " -> ");
24✔
987
    state = handler->tryWrite(pident->outMSG, pident->outPos, pident->outMSG.size());
24✔
988
    TCPLOG(pident->tcpsock, pident->outPos << '/' << pident->outMSG.size() << endl);
24✔
989

990
    if (state == IOState::Done) {
24✔
991
      TCPLOG(pident->tcpsock, "asendtcp success A" << endl);
11✔
992
      return LWResult::Result::Success;
11✔
993
    }
11✔
994
  }
24✔
995
  catch (const std::exception& e) {
24✔
996
    TCPLOG(pident->tcpsock, "tryWrite() exception..." << e.what() << endl);
2✔
997
    return LWResult::Result::PermanentError;
2✔
998
  }
2✔
999

1000
  // Will set pident->lowState
1001
  TCPIOHandlerStateChange(IOState::Done, state, pident);
11✔
1002

1003
  PacketBuffer packet;
11✔
1004
  int ret = g_multiTasker->waitEvent(pident, &packet, g_networkTimeoutMsec);
11✔
1005
  TCPLOG(pident->tcpsock, "asendtcp waitEvent returned " << ret << ' ' << packet.size() << '/' << data.size() << ' ');
11✔
1006
  if (ret == 0) {
11!
1007
    TCPLOG(pident->tcpsock, "timeout" << endl);
×
1008
    TCPIOHandlerStateChange(pident->lowState, IOState::Done, pident);
×
1009
    return LWResult::Result::Timeout;
×
1010
  }
×
1011
  if (ret == -1) { // error
11!
1012
    TCPLOG(pident->tcpsock, "PermanentError" << endl);
×
1013
    TCPIOHandlerStateChange(pident->lowState, IOState::Done, pident);
×
1014
    return LWResult::Result::PermanentError;
×
1015
  }
×
1016
  if (packet.size() != data.size()) { // main loop tells us what it sent out, or empty in case of an error
11!
1017
    // fd housekeeping done by TCPIOHandlerIO
1018
    TCPLOG(pident->tcpsock, "PermanentError size mismatch" << endl);
×
1019
    return LWResult::Result::PermanentError;
×
1020
  }
×
1021

1022
  TCPLOG(pident->tcpsock, "asendtcp success" << endl);
11✔
1023
  return LWResult::Result::Success;
11✔
1024
}
11✔
1025

1026
LWResult::Result arecvtcp(PacketBuffer& data, const size_t len, shared_ptr<TCPIOHandler>& handler, const bool incompleteOkay)
1027
{
36✔
1028
  TCPLOG(handler->getDescriptor(), "arecvtcp called " << len << ' ' << data.size() << endl);
36✔
1029
  data.resize(len);
36✔
1030

1031
  // We might have data already available from the TLS layer, try to get that into the buffer
1032
  size_t pos = 0;
36✔
1033
  IOState state = IOState::Done;
36✔
1034
  try {
36✔
1035
    TCPLOG(handler->getDescriptor(), "calling tryRead() " << len << endl);
36✔
1036
    state = handler->tryRead(data, pos, len);
36✔
1037
    TCPLOG(handler->getDescriptor(), "arcvtcp tryRead() returned " << int(state) << ' ' << pos << '/' << len << endl);
36✔
1038
    switch (state) {
36!
1039
    case IOState::Done:
18✔
1040
    case IOState::NeedRead:
36✔
1041
      if (pos == len || (incompleteOkay && pos > 0)) {
36!
1042
        data.resize(pos);
18✔
1043
        TCPLOG(handler->getDescriptor(), "acecvtcp success A" << endl);
18✔
1044
        return LWResult::Result::Success;
18✔
1045
      }
18✔
1046
      break;
18✔
1047
    case IOState::NeedWrite:
18!
1048
      break;
×
1049
    case IOState::Async:
×
1050
      throw std::runtime_error("TLS async mode not supported");
×
1051
      break;
×
1052
    }
36✔
1053
  }
36✔
1054
  catch (const std::exception& e) {
36✔
1055
    TCPLOG(handler->getDescriptor(), "tryRead() exception..." << e.what() << endl);
×
1056
    return LWResult::Result::PermanentError;
×
1057
  }
×
1058

1059
  auto pident = std::make_shared<PacketID>();
18✔
1060
  pident->tcphandler = handler;
18✔
1061
  pident->tcpsock = handler->getDescriptor();
18✔
1062
  // We might have a partial result
1063
  pident->inMSG = std::move(data);
18✔
1064
  pident->inPos = pos;
18✔
1065
  pident->inWanted = len;
18✔
1066
  pident->inIncompleteOkay = incompleteOkay;
18✔
1067
  pident->highState = TCPAction::DoingRead;
18✔
1068

1069
  data.clear();
18✔
1070

1071
  // Will set pident->lowState
1072
  TCPIOHandlerStateChange(IOState::Done, state, pident);
18✔
1073

1074
  int ret = g_multiTasker->waitEvent(pident, &data, authWaitTimeMSec(g_multiTasker));
18✔
1075
  TCPLOG(pident->tcpsock, "arecvtcp " << ret << ' ' << data.size() << ' ');
18✔
1076
  if (ret == 0) {
18!
1077
    TCPLOG(pident->tcpsock, "timeout" << endl);
×
1078
    TCPIOHandlerStateChange(pident->lowState, IOState::Done, pident);
×
1079
    return LWResult::Result::Timeout;
×
1080
  }
×
1081
  if (ret == -1) {
18!
1082
    TCPLOG(pident->tcpsock, "PermanentError" << endl);
×
1083
    TCPIOHandlerStateChange(pident->lowState, IOState::Done, pident);
×
1084
    return LWResult::Result::PermanentError;
×
1085
  }
×
1086
  if (data.empty()) { // error, EOF or other
18!
1087
    // fd housekeeping done by TCPIOHandlerIO
1088
    TCPLOG(pident->tcpsock, "EOF" << endl);
×
1089
    return LWResult::Result::PermanentError;
×
1090
  }
×
1091

1092
  TCPLOG(pident->tcpsock, "arecvtcp success" << endl);
18✔
1093
  return LWResult::Result::Success;
18✔
1094
}
18✔
1095

1096
// The two last arguments to makeTCPServerSockets are used for logging purposes only
1097
unsigned int makeTCPServerSockets(deferredAdd_t& deferredAdds, std::set<int>& tcpSockets, Logr::log_t log, bool doLog, unsigned int instances)
1098
{
175✔
1099
  vector<string> localAddresses;
175✔
1100
  vector<string> logVec;
175✔
1101
  stringtok(localAddresses, ::arg()["local-address"], " ,");
175✔
1102

1103
  if (localAddresses.empty()) {
175!
1104
    throw PDNSException("No local address specified");
×
1105
  }
×
1106

1107
#ifdef TCP_DEFER_ACCEPT
175✔
1108
  auto first = true;
175✔
1109
#endif
175✔
1110
  const uint16_t defaultLocalPort = ::arg().asNum("local-port");
175✔
1111
  const vector<string> defaultVector = {"127.0.0.1", "::1"};
175✔
1112
  const auto configIsDefault = localAddresses == defaultVector;
175✔
1113

1114
  for (const auto& localAddress : localAddresses) {
192✔
1115
    ComboAddress address{localAddress, defaultLocalPort};
192✔
1116
    auto socketFd = FDWrapper(socket(address.sin6.sin6_family, SOCK_STREAM, 0));
192✔
1117
    if (socketFd < 0) {
192!
1118
      throw PDNSException("Making a TCP server socket for resolver: " + stringerror());
×
1119
    }
×
1120
    setCloseOnExec(socketFd);
192✔
1121

1122
    int tmp = 1;
192✔
1123
    if (setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &tmp, sizeof tmp) < 0) {
192!
1124
      int err = errno;
×
1125
      SLOG(g_log << Logger::Error << "Setsockopt failed for TCP listening socket" << endl,
×
1126
           log->error(Logr::Critical, err, "Setsockopt failed for TCP listening socket"));
×
1127
      _exit(1);
×
1128
    }
×
1129
    if (address.sin6.sin6_family == AF_INET6 && setsockopt(socketFd, IPPROTO_IPV6, IPV6_V6ONLY, &tmp, sizeof(tmp)) < 0) {
192!
1130
      int err = errno;
×
1131
      SLOG(g_log << Logger::Error << "Failed to set IPv6 socket to IPv6 only, continuing anyhow: " << stringerror(err) << endl,
×
1132
           log->error(Logr::Warning, err, "Failed to set IPv6 socket to IPv6 only, continuing anyhow"));
×
1133
    }
×
1134

1135
#ifdef TCP_DEFER_ACCEPT
192✔
1136
    if (setsockopt(socketFd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &tmp, sizeof tmp) >= 0) {
192!
1137
      if (first) {
192✔
1138
        SLOG(g_log << Logger::Info << "Enabled TCP data-ready filter for (slight) DoS protection" << endl,
175✔
1139
             log->info(Logr::Info, "Enabled TCP data-ready filter for (slight) DoS protection"));
175✔
1140
      }
175✔
1141
    }
192✔
1142
#endif
192✔
1143

1144
    if (::arg().mustDo("non-local-bind")) {
192!
1145
      Utility::setBindAny(AF_INET, socketFd);
×
1146
    }
×
1147

1148
    if (g_reusePort) {
192✔
1149
#if defined(SO_REUSEPORT_LB)
1150
      try {
1151
        SSetsockopt(socketFd, SOL_SOCKET, SO_REUSEPORT_LB, 1);
1152
      }
1153
      catch (const std::exception& e) {
1154
        throw PDNSException(std::string("SO_REUSEPORT_LB: ") + e.what());
1155
      }
1156
#elif defined(SO_REUSEPORT)
1157
      try {
162✔
1158
        SSetsockopt(socketFd, SOL_SOCKET, SO_REUSEPORT, 1);
162✔
1159
      }
162✔
1160
      catch (const std::exception& e) {
162✔
1161
        throw PDNSException(std::string("SO_REUSEPORT: ") + e.what());
×
1162
      }
×
1163
#endif
162✔
1164
    }
162✔
1165

1166
    if (SyncRes::s_tcp_fast_open > 0) {
192!
1167
      checkFastOpenSysctl(false, log);
×
1168
#ifdef TCP_FASTOPEN
×
1169
      if (setsockopt(socketFd, IPPROTO_TCP, TCP_FASTOPEN, &SyncRes::s_tcp_fast_open, sizeof SyncRes::s_tcp_fast_open) < 0) {
×
1170
        int err = errno;
×
1171
        SLOG(g_log << Logger::Error << "Failed to enable TCP Fast Open for listening socket: " << stringerror(err) << endl,
×
1172
             log->error(Logr::Error, err, "Failed to enable TCP Fast Open for listening socket"));
×
1173
      }
×
1174
#else
1175
      SLOG(g_log << Logger::Warning << "TCP Fast Open configured but not supported for listening socket" << endl,
1176
           log->info(Logr::Warning, "TCP Fast Open configured but not supported for listening socket"));
1177
#endif
1178
    }
×
1179

1180
    socklen_t socklen = address.sin4.sin_family == AF_INET ? sizeof(address.sin4) : sizeof(address.sin6);
192✔
1181
    if (::bind(socketFd, reinterpret_cast<struct sockaddr*>(&address), socklen) < 0) { // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
192!
1182
      int err = errno;
×
1183
      if (!configIsDefault || address != ComboAddress{"::1", defaultLocalPort}) {
×
1184
        throw PDNSException("Binding TCP server socket for " + address.toStringWithPort() + ": " + stringerror(err));
×
1185
      }
×
1186
      log->info(Logr::Warning, "Cannot listen on this address, skipping", "proto", Logging::Loggable("TCP"), "address", Logging::Loggable(address), "error", Logging::Loggable(stringerror(err)));
×
1187
      continue;
×
1188
    }
×
1189

1190
    setNonBlocking(socketFd);
192✔
1191
    try {
192✔
1192
      setSocketSendBuffer(socketFd, 65000);
192✔
1193
    }
192✔
1194
    catch (const std::exception& e) {
192✔
1195
      SLOG(g_log << Logger::Error << e.what() << endl,
×
1196
           log->error(Logr::Error, e.what(), "Exception while setting socket send buffer"));
×
1197
    }
×
1198

1199
    listen(socketFd, 128);
192✔
1200
    deferredAdds.emplace_back(socketFd, handleNewTCPQuestion);
192✔
1201
    tcpSockets.insert(socketFd);
192✔
1202
    logVec.emplace_back(address.toStringWithPort());
192✔
1203

1204
    // we don't need to update g_listenSocketsAddresses since it doesn't work for TCP/IP:
1205
    //  - fd is not that which we know here, but returned from accept()
1206

1207
#ifdef TCP_DEFER_ACCEPT
192✔
1208
    first = false;
192✔
1209
#endif
192✔
1210
    socketFd.release(); // to avoid auto-close by FDWrapper
192✔
1211
  }
192✔
1212
  if (doLog) {
175!
1213
    log->info(Logr::Info, "Listening for queries", "protocol", Logging::Loggable("TCP"), "addresses", Logging::IterLoggable(logVec.cbegin(), logVec.cend()), "socketInstances", Logging::Loggable(instances), "reuseport", Logging::Loggable(g_reusePort));
175✔
1214
  }
175✔
1215
  return localAddresses.size();
175✔
1216
}
175✔
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