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

PowerDNS / pdns / 20618548088

31 Dec 2025 12:00PM UTC coverage: 72.648% (-0.7%) from 73.336%
20618548088

Pull #16693

github

web-flow
Merge 3f7d9a75b into 65de281db
Pull Request #16693: auth: plumbing for structured logging

39009 of 65430 branches covered (59.62%)

Branch coverage included in aggregate %.

807 of 2400 new or added lines in 58 files covered. (33.63%)

200 existing lines in 39 files now uncovered.

129187 of 166092 relevant lines covered (77.78%)

5266744.49 hits per line

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

53.33
/modules/pipebackend/pipebackend.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 <string>
26
#include <map>
27
#include <unistd.h>
28
#include <stdlib.h>
29
#include <sstream>
30
#include "coprocess.hh"
31

32
#include "pdns/namespaces.hh"
33

34
#include "pdns/dns.hh"
35
#include "pdns/dnsbackend.hh"
36
#include "pdns/dnspacket.hh"
37
#include "pdns/pdnsexception.hh"
38
#include "pdns/logger.hh"
39
#include "pdns/arguments.hh"
40
#include <sys/socket.h>
41
#include <netinet/in.h>
42
#include <arpa/inet.h>
43
#include "pipebackend.hh"
44

45
// The following requirement guarantees UnknownDomainID will get output as "-1"
46
// for compatibility.
47
static_assert(std::is_signed<domainid_t>::value);
48

49
static const char* kBackendId = "[PIPEBackend]";
50

51
CoWrapper::CoWrapper(std::shared_ptr<Logr::Logger> log, const string& command, int timeout, int abiVersion)
52
{
10✔
53
  d_command = command;
10✔
54
  d_timeout = timeout;
10✔
55
  d_abiVersion = abiVersion;
10✔
56
  d_slog = log;
10✔
57
  launch(); // let exceptions fall through - if initial launch fails, we want to die
10✔
58
  // I think
59
}
10✔
60

61
CoWrapper::~CoWrapper() = default;
2✔
62

63
void CoWrapper::launch()
64
{
54✔
65
  if (d_cp)
54✔
66
    return;
44✔
67

68
  if (d_command.empty())
10!
69
    throw ArgException("pipe-command is not specified");
×
70

71
  if (isUnixSocket(d_command)) {
10!
72
    d_cp = std::make_unique<UnixRemote>(d_command);
×
73
  }
×
74
  else {
10✔
75
    auto coprocess = std::make_unique<CoProcess>(d_command, d_timeout);
10✔
76
    coprocess->launch();
10✔
77
    d_cp = std::move(coprocess);
10✔
78
  }
10✔
79

80
  d_cp->send("HELO\t" + std::to_string(d_abiVersion));
10✔
81
  string banner;
10✔
82
  d_cp->receive(banner);
10✔
83
  SLOG(g_log << Logger::Error << "Backend launched with banner: " << banner << endl,
10!
84
       d_slog->info(Logr::Info, "launched", "banner", Logging::Loggable(banner)));
10✔
85
}
10✔
86

87
void CoWrapper::send(const string& line)
88
{
17✔
89
  launch();
17✔
90
  try {
17✔
91
    d_cp->send(line);
17✔
92
    return;
17✔
93
  }
17✔
94
  catch (PDNSException& ae) {
17✔
95
    d_cp.reset();
×
96
    throw;
×
97
  }
×
98
}
17✔
99
void CoWrapper::receive(string& line)
100
{
27✔
101
  launch();
27✔
102
  try {
27✔
103
    d_cp->receive(line);
27✔
104
    return;
27✔
105
  }
27✔
106
  catch (PDNSException& ae) {
27✔
NEW
107
    SLOG(g_log << Logger::Warning << kBackendId << " Unable to receive data from coprocess. " << ae.reason << endl,
×
NEW
108
         d_slog->error(Logr::Warning, ae.reason, "Unable to receive data from coprocess"));
×
109
    d_cp.reset();
×
110
    throw;
×
111
  }
×
112
}
27✔
113

114
PipeBackend::PipeBackend(const string& suffix)
115
{
10✔
116
  if (g_slogStructured) {
10!
NEW
117
    d_slog = g_slog->withName("pipe" + suffix);
×
NEW
118
  }
×
119
  d_disavow = false;
10✔
120
  setArgPrefix("pipe" + suffix);
10✔
121
  try {
10✔
122
    launch();
10✔
123
  }
10✔
124
  catch (const ArgException& A) {
10✔
NEW
125
    SLOG(g_log << Logger::Error << kBackendId << " Unable to launch, fatal argument error: " << A.reason << endl,
×
NEW
126
         d_slog->error(Logr::Error, A.reason, "Unable to launch coprocess"));
×
127
    throw;
×
128
  }
×
129
  catch (...) {
10✔
130
    throw;
×
131
  }
×
132
}
10✔
133

134
void PipeBackend::launch()
135
{
54✔
136
  if (d_coproc)
54✔
137
    return;
44✔
138

139
  try {
10✔
140
    if (!getArg("regex").empty()) {
10!
141
      d_regex = std::make_unique<Regex>(getArg("regex"));
×
142
    }
×
143
    d_regexstr = getArg("regex");
10✔
144
    d_abiVersion = getArgAsNum("abi-version");
10✔
145
    d_coproc = std::make_unique<CoWrapper>(d_slog, getArg("command"), getArgAsNum("timeout"), getArgAsNum("abi-version"));
10✔
146
  }
10✔
147

148
  catch (const ArgException& A) {
10✔
149
    cleanup();
×
150
    throw;
×
151
  }
×
152
}
10✔
153

154
/*
155
 * Cleans up the co-process wrapper
156
 */
157
void PipeBackend::cleanup()
158
{
2✔
159
  d_coproc.reset(nullptr);
2✔
160
  d_regex.reset();
2✔
161
  d_regexstr = string();
2✔
162
  d_abiVersion = 0;
2✔
163
}
2✔
164

165
void PipeBackend::lookup(const QType& qtype, const DNSName& qname, domainid_t zoneId, DNSPacket* pkt_p)
166
{
17✔
167
  try {
17✔
168
    launch();
17✔
169
    d_disavow = false;
17✔
170
    if (d_regex && !d_regex->match(qname.toStringRootDot())) {
17!
NEW
171
      if (::arg().mustDo("query-logging")) {
×
NEW
172
        SLOG(g_log << Logger::Error << "Query for '" << qname << "' failed regex '" << d_regexstr << "'" << endl,
×
NEW
173
             d_slog->info(Logr::Warning, "query failed regex", "name", Logging::Loggable(qname), "regex", Logging::Loggable(d_regexstr)));
×
NEW
174
      }
×
175
      d_disavow = true; // don't pass to backend
×
176
    }
×
177
    else {
17✔
178
      ostringstream query;
17✔
179
      string localIP = "0.0.0.0";
17✔
180
      string remoteIP = "0.0.0.0";
17✔
181
      Netmask realRemote("0.0.0.0/0");
17✔
182
      if (pkt_p) {
17✔
183
        localIP = pkt_p->getLocal().toString();
8✔
184
        realRemote = pkt_p->getRealRemote();
8✔
185
        remoteIP = pkt_p->getInnerRemote().toString();
8✔
186
      }
8✔
187
      // abi-version = 1
188
      // type    qname           qclass  qtype   id      remote-ip-address
189
      query << "Q\t" << qname.toStringRootDot() << "\tIN\t" << qtype.toString() << "\t" << zoneId << "\t" << remoteIP;
17✔
190

191
      // add the local-ip-address if abi-version is set to 2
192
      if (d_abiVersion >= 2)
17✔
193
        query << "\t" << localIP;
13✔
194
      if (d_abiVersion >= 3)
17✔
195
        query << "\t" << realRemote.toString();
13✔
196

197
      if (::arg().mustDo("query-logging")) {
17!
NEW
198
        SLOG(g_log << Logger::Error << "Query: '" << query.str() << "'" << endl,
×
NEW
199
             d_slog->info(Logr::Warning, "sending query", "query", Logging::Loggable(query.str())));
×
NEW
200
      }
×
201
      d_coproc->send(query.str());
17✔
202
    }
17✔
203
  }
17✔
204
  catch (PDNSException& pe) {
17✔
NEW
205
    SLOG(g_log << Logger::Error << kBackendId << " Error from coprocess: " << pe.reason << endl,
×
NEW
206
         d_slog->error(Logr::Error, pe.reason, "Error from coprocess"));
×
207
    d_disavow = true;
×
208
  }
×
209
  d_qtype = qtype;
17✔
210
  d_qname = qname;
17✔
211
}
17✔
212

213
bool PipeBackend::list(const ZoneName& target, domainid_t domain_id, bool /* include_disabled */)
214
{
×
215
  try {
×
216
    launch();
×
217
    d_disavow = false;
×
218
    ostringstream query;
×
219
    // The question format:
220

221
    // type    qname           qclass  qtype   id      ip-address
222
    if (d_abiVersion >= 4)
×
223
      query << "AXFR\t" << domain_id << "\t" << target.toStringRootDot();
×
224
    else
×
225
      query << "AXFR\t" << domain_id;
×
226

227
    d_coproc->send(query.str());
×
228
  }
×
229
  catch (PDNSException& ae) {
×
NEW
230
    SLOG(g_log << Logger::Error << kBackendId << " Error from coprocess: " << ae.reason << endl,
×
NEW
231
         d_slog->error(Logr::Error, ae.reason, "Error from coprocess"));
×
232
  }
×
233
  d_qname = DNSName(std::to_string(domain_id)); // why do we store a number here??
×
234
  return true;
×
235
}
×
236

237
string PipeBackend::directBackendCmd(const string& query)
238
{
×
239
  if (d_abiVersion < 5)
×
240
    return "not supported on ABI version " + std::to_string(d_abiVersion) + " (use ABI version 5 or later)\n";
×
241

242
  try {
×
243
    launch();
×
244
    ostringstream oss;
×
245
    oss << "CMD\t" << query;
×
246
    d_coproc->send(oss.str());
×
247
  }
×
248
  catch (PDNSException& ae) {
×
NEW
249
    SLOG(g_log << Logger::Error << kBackendId << " Error from coprocess: " << ae.reason << endl,
×
NEW
250
         d_slog->error(Logr::Error, ae.reason, "Error from coprocess"));
×
251
    cleanup();
×
252
  }
×
253

254
  ostringstream oss;
×
255
  while (true) {
×
256
    string line;
×
257
    d_coproc->receive(line);
×
258
    if (line == "END")
×
259
      break;
×
260
    oss << line << std::endl;
×
261
  };
×
262

263
  return oss.str();
×
264
}
×
265

266
PipeBackend::~PipeBackend()
267
{
2✔
268
  cleanup();
2✔
269
}
2✔
270

271
void PipeBackend::throwTooShortDataError(const std::string& what)
272
{
×
NEW
273
  SLOG(g_log << Logger::Error << kBackendId << " Coprocess returned incomplete or empty line in data section for query for " << d_qname << endl,
×
NEW
274
       d_slog->info(Logr::Error, "Coprocess returned incomplete or empty line", "query", Logging::Loggable(d_qname)));
×
275
  throw PDNSException("Format error communicating with coprocess in data section" + what);
×
276
}
×
277

278
bool PipeBackend::get(DNSResourceRecord& r)
279
{
27✔
280
  if (d_disavow) // this query has been blocked
27!
281
    return false;
×
282

283
  string line;
27✔
284

285
  // The answer format:
286
  // DATA    qname           qclass  qtype   ttl     id      content
287

288
  try {
27✔
289
    launch();
27✔
290
    for (;;) {
27✔
291
      d_coproc->receive(line);
27✔
292
      vector<string> parts;
27✔
293
      stringtok(parts, line, "\t");
27✔
294
      if (parts.empty()) {
27!
NEW
295
        SLOG(g_log << Logger::Error << kBackendId << " Coprocess returned empty line in query for " << d_qname << endl,
×
NEW
296
             d_slog->info(Logr::Error, "Coprocess returned empty line", "query", Logging::Loggable(d_qname)));
×
297
        throw PDNSException("Format error communicating with coprocess");
×
298
      }
×
299
      else if (parts[0] == "FAIL") {
27!
300
        throw DBException("coprocess returned a FAIL");
×
301
      }
×
302
      else if (parts[0] == "END") {
27✔
303
        return false;
17✔
304
      }
17✔
305
      else if (parts[0] == "LOG") {
10!
NEW
306
        SLOG(g_log << Logger::Error << "Coprocess: " << line.substr(4) << endl,
×
NEW
307
             d_slog->info(Logr::Error, "Coprocess log message", "log", Logging::Loggable(line.substr(4))));
×
308
        continue;
×
309
      }
×
310
      else if (parts[0] == "DATA") { // yay
10!
311
        // The shortest records (ENT) require 6 fields. Other may require more
312
        // and will have a stricter check once the record type has been
313
        // computed.
314
        if (parts.size() < 6 + (d_abiVersion >= 3 ? 2 : 0)) {
10!
315
          throwTooShortDataError("");
×
316
        }
×
317

318
        if (d_abiVersion >= 3) {
10✔
319
          r.scopeMask = std::stoi(parts[1]);
8✔
320
          r.auth = (parts[2] == "1");
8✔
321
          parts.erase(parts.begin() + 1, parts.begin() + 3);
8✔
322
        }
8✔
323
        else {
2✔
324
          r.scopeMask = 0;
2✔
325
          r.auth = true;
2✔
326
        }
2✔
327
        r.qname = DNSName(parts[1]);
10✔
328
        r.qtype = parts[3];
10✔
329
        pdns::checked_stoi_into(r.ttl, parts[4]);
10✔
330
        pdns::checked_stoi_into(r.domain_id, parts[5]);
10✔
331

332
        switch (r.qtype.getCode()) {
10✔
333
        case QType::ENT:
×
334
          // No other data to process
335
          r.content.clear();
×
336
          break;
×
337
        case QType::MX:
1✔
338
        case QType::SRV:
1!
339
          if (parts.size() < 8) {
1!
340
            throwTooShortDataError(" of MX/SRV record");
×
341
          }
×
342
          r.content = parts[6] + " " + parts[7];
1✔
343
          break;
1✔
344
        default:
9✔
345
          if (parts.size() < 7) {
9!
346
            throwTooShortDataError("");
×
347
          }
×
348
          r.content = parts[6];
9✔
349
          for (std::vector<std::string>::size_type pos = 7; pos < parts.size(); ++pos) {
9!
350
            r.content.append(1, ' ');
×
351
            r.content.append(parts[pos]);
×
352
          }
×
353
          break;
9✔
354
        }
10✔
355
        break;
10✔
356
      }
10✔
357
      else
×
358
        throw PDNSException("Coprocess backend sent incorrect response '" + line + "'");
×
359
    }
27✔
360
  }
27✔
361
  catch (DBException& dbe) {
27✔
NEW
362
    SLOG(g_log << Logger::Error << kBackendId << " " << dbe.reason << endl,
×
NEW
363
         d_slog->error(Logr::Error, dbe.reason, "Coprocess error"));
×
364
    throw;
×
365
  }
×
366
  catch (PDNSException& pe) {
27✔
NEW
367
    SLOG(g_log << Logger::Error << kBackendId << " " << pe.reason << endl,
×
NEW
368
         d_slog->error(Logr::Error, pe.reason, "Coprocess error"));
×
369
    cleanup();
×
370
    throw;
×
371
  }
×
372
  return true;
10✔
373
}
27✔
374

375
//
376
// Magic class that is activated when the dynamic library is loaded
377
//
378

379
class PipeFactory : public BackendFactory
380
{
381
public:
382
  PipeFactory() :
383
    BackendFactory("pipe") {}
5,904✔
384

385
  void declareArguments(const string& suffix = "") override
386
  {
2✔
387
    declare(suffix, "command", "Command to execute for piping questions to", "");
2✔
388
    declare(suffix, "timeout", "Number of milliseconds to wait for an answer", "2000");
2✔
389
    declare(suffix, "regex", "Regular expression of queries to pass to coprocess", "");
2✔
390
    declare(suffix, "abi-version", "Version of the pipe backend ABI", "1");
2✔
391
  }
2✔
392

393
  DNSBackend* make(const string& suffix = "") override
394
  {
10✔
395
    return new PipeBackend(suffix);
10✔
396
  }
10✔
397
};
398

399
class PipeLoader
400
{
401
public:
402
  PipeLoader()
403
  {
5,904✔
404
    BackendMakers().report(std::make_unique<PipeFactory>());
5,904✔
405
    // If this module is not loaded dynamically at runtime, this code runs
406
    // as part of a global constructor, before the structured logger has a
407
    // chance to be set up, so fallback to simple logging in this case.
408
    if (!g_slogStructured || !g_slog) {
5,904!
409
      g_log << Logger::Info << kBackendId << " This is the pipe backend version " VERSION
5,904✔
410
#ifndef REPRODUCIBLE
5,904✔
411
            << " (" __DATE__ " " __TIME__ ")"
5,904✔
412
#endif
5,904✔
413
            << " reporting" << endl;
5,904✔
414
    }
5,904✔
NEW
415
    else {
×
NEW
416
      g_slog->withName("pipebackend")->info(Logr::Info, "pipe backend starting", "version", Logging::Loggable(VERSION)
×
UNCOV
417
#ifndef REPRODUCIBLE
×
NEW
418
                                                                                              ,
×
NEW
419
                                            "build date", Logging::Loggable(__DATE__ " " __TIME__)
×
UNCOV
420
#endif
×
NEW
421
      );
×
NEW
422
    }
×
423
  }
5,904✔
424
};
425

426
static PipeLoader pipeloader;
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

© 2026 Coveralls, Inc