2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
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.
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.
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.
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.
23 #include "dnsparser.hh"
28 #include <boost/program_options.hpp>
29 #include <arpa/inet.h>
30 #include <sys/types.h>
36 #include "threadname.hh"
39 #include <condition_variable>
43 #include "ixfrutils.hh"
44 #include "axfr-retriever.hh"
45 #include "dns_random.hh"
51 #include "communicator.hh"
52 #include "query-local-address.hh"
54 #include "ixfrdist-stats.hh"
55 #include "ixfrdist-web.hh"
56 #pragma GCC diagnostic push
57 #pragma GCC diagnostic ignored "-Wshadow"
58 #include <yaml-cpp/yaml.h>
59 #pragma GCC diagnostic pop
60 #include "auth-packetcache.hh"
61 #include "auth-querycache.hh"
62 #include "auth-zonecache.hh"
64 /* BEGIN Needed because of deeper dependencies */
65 #include "arguments.hh"
68 // NOLINTNEXTLINE(readability-identifier-length)
70 // NOLINTNEXTLINE(readability-identifier-length)
72 AuthZoneCache g_zoneCache
;
76 static ArgvMap theArg
;
79 /* END Needed because of deeper dependencies */
81 // Allows reading/writing ComboAddresses and ZoneNames in YAML-cpp
84 struct convert
<ComboAddress
> {
85 static Node
encode(const ComboAddress
& rhs
) {
86 return Node(rhs
.toStringWithPort());
88 static bool decode(const Node
& node
, ComboAddress
& rhs
) {
89 if (!node
.IsScalar()) {
93 rhs
= ComboAddress(node
.as
<string
>(), 53);
95 } catch(const runtime_error
&e
) {
97 } catch (const PDNSException
&e
) {
104 struct convert
<ZoneName
> {
105 static Node
encode(const ZoneName
& rhs
) {
106 return Node(rhs
.toStringRootDot());
108 static bool decode(const Node
& node
, ZoneName
& rhs
) {
109 if (!node
.IsScalar()) {
113 rhs
= ZoneName(node
.as
<string
>());
115 } catch(const runtime_error
&e
) {
117 } catch (const PDNSException
&e
) {
124 struct convert
<Netmask
> {
125 static Node
encode(const Netmask
& rhs
) {
126 return Node(rhs
.toString());
128 static bool decode(const Node
& node
, Netmask
& rhs
) {
129 if (!node
.IsScalar()) {
133 rhs
= Netmask(node
.as
<string
>());
135 } catch(const runtime_error
&e
) {
137 } catch (const PDNSException
&e
) {
145 shared_ptr
<const SOARecordContent
> oldSOA
;
146 shared_ptr
<const SOARecordContent
> newSOA
;
147 vector
<DNSRecord
> removals
;
148 vector
<DNSRecord
> additions
;
154 shared_ptr
<const SOARecordContent
> soa
; // The SOA of the latest AXFR
155 records_t latestAXFR
; // The most recent AXFR
156 vector
<std::shared_ptr
<ixfrdiff_t
>> ixfrDiffs
;
160 // Why a struct? This way we can add more options to a domain in the future
161 struct ixfrdistdomain_t
{
162 set
<ComboAddress
> primaries
; // A set so we can do multiple primary addresses in the future
163 std::set
<ComboAddress
> notify
; // Set of addresses to forward NOTIFY to
164 uint32_t maxSOARefresh
{0}; // Cap SOA refresh value to the given value in seconds
167 // This contains the configuration for each domain
168 static map
<ZoneName
, ixfrdistdomain_t
> g_domainConfigs
;
170 // Map domains and their data
171 static LockGuarded
<std::map
<ZoneName
, std::shared_ptr
<ixfrinfo_t
>>> g_soas
;
173 // Queue of received NOTIFYs, already verified against their primary IPs
174 // Lazily implemented as a set
175 static LockGuarded
<std::set
<ZoneName
>> g_notifiesReceived
;
177 // Queue of outgoing NOTIFY
178 static LockGuarded
<NotificationQueue
> g_notificationQueue
;
180 // Condition variable for TCP handling
181 static std::condition_variable g_tcpHandlerCV
;
182 static std::queue
<pair
<int, ComboAddress
>> g_tcpRequestFDs
;
183 static std::mutex g_tcpRequestFDsMutex
;
185 namespace po
= boost::program_options
;
187 static bool g_exiting
= false;
189 static NetmaskGroup g_acl
; // networks that can QUERY us
190 static NetmaskGroup g_notifySources
; // networks (well, IPs) that can NOTIFY us
191 static bool g_compress
= false;
193 static ixfrdistStats g_stats
;
195 // g_stats is static, so local to this file. But the webserver needs this info
196 string
doGetStats() {
197 return g_stats
.getStats();
200 static void handleSignal(int signum
) {
201 g_log
<<Logger::Notice
<<"Got "<<strsignal(signum
)<<" signal";
203 g_log
<<Logger::Notice
<<", this is the second time we were asked to stop, forcefully exiting"<<endl
;
206 g_log
<<Logger::Notice
<<", stopping, this may take a few second due to in-progress transfers and cleanup. Send this signal again to forcefully stop"<<endl
;
210 static void usage(po::options_description
&desc
) {
211 cerr
<< "Usage: ixfrdist [OPTION]..."<<endl
;
212 cerr
<< desc
<< "\n";
215 // The compiler does not like using rfc1982LessThan in std::sort directly
216 static bool sortSOA(uint32_t i
, uint32_t j
) {
217 return rfc1982LessThan(i
, j
);
220 static void cleanUpDomain(const ZoneName
& domain
, const uint16_t& keep
, const string
& workdir
) {
221 string dir
= workdir
+ "/" + domain
.toString();
222 vector
<uint32_t> zoneVersions
;
223 auto directoryError
= pdns::visit_directory(dir
, [&zoneVersions
]([[maybe_unused
]] ino_t inodeNumber
, const std::string_view
& name
) {
224 if (name
!= "." && name
!= "..") {
226 auto version
= pdns::checked_stoi
<uint32_t>(std::string(name
));
227 zoneVersions
.push_back(version
);
235 if (directoryError
) {
239 g_log
<<Logger::Info
<<"Found "<<zoneVersions
.size()<<" versions of "<<domain
<<", asked to keep "<<keep
<<", ";
240 if (zoneVersions
.size() <= keep
) {
241 g_log
<<Logger::Info
<<"not cleaning up"<<endl
;
244 g_log
<<Logger::Info
<<"cleaning up the oldest "<<zoneVersions
.size() - keep
<<endl
;
247 std::sort(zoneVersions
.begin(), zoneVersions
.end(), sortSOA
);
249 // And delete all the old ones
251 // Lock to ensure no one reads this.
252 auto lock
= g_soas
.lock();
253 for (auto iter
= zoneVersions
.cbegin(); iter
!= zoneVersions
.cend() - keep
; ++iter
) {
254 string fname
= dir
+ "/" + std::to_string(*iter
);
255 g_log
<<Logger::Debug
<<"Removing "<<fname
<<endl
;
256 unlink(fname
.c_str());
261 static void getSOAFromRecords(const records_t
& records
, shared_ptr
<const SOARecordContent
>& soa
, uint32_t& soaTTL
) {
262 for (const auto& dnsrecord
: records
) {
263 if (dnsrecord
.d_type
== QType::SOA
) {
264 soa
= getRR
<SOARecordContent
>(dnsrecord
);
265 if (soa
== nullptr) {
266 throw PDNSException("Unable to determine SOARecordContent from old records");
268 soaTTL
= dnsrecord
.d_ttl
;
272 throw PDNSException("No SOA in supplied records");
275 static void makeIXFRDiff(const records_t
& from
, const records_t
& to
, std::shared_ptr
<ixfrdiff_t
>& diff
, const shared_ptr
<const SOARecordContent
>& fromSOA
= nullptr, uint32_t fromSOATTL
=0, const shared_ptr
<const SOARecordContent
>& toSOA
= nullptr, uint32_t toSOATTL
= 0) {
276 set_difference(from
.cbegin(), from
.cend(), to
.cbegin(), to
.cend(), back_inserter(diff
->removals
), from
.value_comp());
277 set_difference(to
.cbegin(), to
.cend(), from
.cbegin(), from
.cend(), back_inserter(diff
->additions
), from
.value_comp());
278 diff
->oldSOA
= fromSOA
;
279 diff
->oldSOATTL
= fromSOATTL
;
280 if (fromSOA
== nullptr) {
281 getSOAFromRecords(from
, diff
->oldSOA
, diff
->oldSOATTL
);
283 diff
->newSOA
= toSOA
;
284 diff
->newSOATTL
= toSOATTL
;
285 if (toSOA
== nullptr) {
286 getSOAFromRecords(to
, diff
->newSOA
, diff
->newSOATTL
);
290 /* you can _never_ alter the content of the resulting shared pointer */
291 static std::shared_ptr
<ixfrinfo_t
> getCurrentZoneInfo(const ZoneName
& domain
)
293 return (*g_soas
.lock())[domain
];
296 static void updateCurrentZoneInfo(const ZoneName
& domain
, std::shared_ptr
<ixfrinfo_t
>& newInfo
)
298 auto soas
= g_soas
.lock();
299 (*soas
)[domain
] = newInfo
;
300 g_stats
.setSOASerial(domain
, newInfo
->soa
->d_st
.serial
);
301 // FIXME: also report zone size?
304 static void sendNotification(int sock
, const ZoneName
& domain
, const ComboAddress
& remote
, uint16_t notificationId
)
306 std::vector
<std::string
> meta
;
307 std::vector
<uint8_t> packet
;
308 DNSPacketWriter
packetWriter(packet
, domain
.operator const DNSName
&(), QType::SOA
, 1, Opcode::Notify
);
309 packetWriter
.getHeader()->id
= notificationId
;
310 packetWriter
.getHeader()->aa
= true;
312 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
313 if (sendto(sock
, packet
.data(), packet
.size(), 0, reinterpret_cast<const struct sockaddr
*>(&remote
), remote
.getSocklen()) < 0) {
314 throw std::runtime_error("Unable to send notify to " + remote
.toStringWithPort() + ": " + stringerror());
318 static void communicatorReceiveNotificationAnswers(const int sock4
, const int sock6
)
320 std::set
<int> fds
= {sock4
};
325 std::array
<char, 1500> buffer
{};
328 // receive incoming notification answers on the nonblocking sockets and take them off the list
329 while (waitForMultiData(fds
, 0, 0, &sock
) > 0) {
330 Utility::socklen_t fromlen
= sizeof(from
);
331 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
332 const auto size
= recvfrom(sock
, buffer
.data(), buffer
.size(), 0, reinterpret_cast<struct sockaddr
*>(&from
), &fromlen
);
336 DNSPacket
packet(true);
337 packet
.setRemote(&from
);
339 if (packet
.parse(buffer
.data(), (size_t)size
) < 0) {
340 g_log
<< Logger::Warning
<< "Unable to parse SOA notification answer from " << packet
.getRemote() << endl
;
344 if (packet
.d
.rcode
!= 0) {
345 g_log
<< Logger::Warning
<< "Received unsuccessful notification report for '" << packet
.qdomain
<< "' from " << from
.toStringWithPort() << ", error: " << RCode::to_s(packet
.d
.rcode
) << endl
;
348 if (g_notificationQueue
.lock()->removeIf(from
, packet
.d
.id
, ZoneName(packet
.qdomain
))) {
349 g_log
<< Logger::Notice
<< "Removed from notification list: '" << packet
.qdomain
<< "' to " << from
.toStringWithPort() << " " << (packet
.d
.rcode
!= 0 ? RCode::to_s(packet
.d
.rcode
) : "(was acknowledged)") << endl
;
352 g_log
<< Logger::Warning
<< "Received spurious notify answer for '" << packet
.qdomain
<< "' from " << from
.toStringWithPort() << endl
;
357 static void communicatorSendNotifications(const int sock4
, const int sock6
)
360 string destinationIp
;
361 uint16_t notificationId
= 0;
364 while (g_notificationQueue
.lock()->getOne(domain
, destinationIp
, ¬ificationId
, purged
)) {
366 ComboAddress
remote(destinationIp
, 53); // default to 53
367 if (remote
.sin4
.sin_family
== AF_INET
) {
368 sendNotification(sock4
, domain
, remote
, notificationId
);
369 } else if (sock6
> 0) {
370 sendNotification(sock6
, domain
, remote
, notificationId
);
372 g_log
<< Logger::Warning
<< "Unable to notify " << destinationIp
<< " for " << domain
<< " as v6 support is not enabled" << std::endl
;
375 g_log
<< Logger::Warning
<< "Notification for " << domain
<< " to " << destinationIp
<< " failed after retries" << std::endl
;
380 static void communicatorThread()
382 setThreadName("ixfrdist/communicator");
383 auto sock4
= makeQuerySocket(pdns::getQueryLocalAddress(AF_INET
, 0), true);
384 auto sock6
= makeQuerySocket(pdns::getQueryLocalAddress(AF_INET6
, 0), true);
387 throw std::runtime_error("Unable to create local query socket");
389 // sock6 can be negative if there is no v6 support, but this is handled later while sending notifications
393 g_log
<< Logger::Notice
<< "Communicator thread stopped" << std::endl
;
396 communicatorReceiveNotificationAnswers(sock4
, sock6
);
397 communicatorSendNotifications(sock4
, sock6
);
398 std::this_thread::sleep_for(std::chrono::seconds(1));
408 static void updateThread(const string
& workdir
, const uint16_t& keep
, const uint16_t& axfrTimeout
, const uint16_t& soaRetry
, const uint32_t axfrMaxRecords
) { // NOLINT(readability-function-cognitive-complexity) 13400 https://github.com/PowerDNS/pdns/issues/13400 Habbie: ixfrdist: reduce complexity
409 setThreadName("ixfrdist/update");
410 std::map
<ZoneName
, time_t> lastCheck
;
412 // Initialize the serials we have
413 for (const auto &domainConfig
: g_domainConfigs
) {
414 ZoneName domain
= domainConfig
.first
;
415 lastCheck
[domain
] = 0;
416 string dir
= workdir
+ "/" + domain
.toString();
418 g_log
<<Logger::Info
<<"Trying to initially load domain "<<domain
<<" from disk"<<endl
;
419 auto serial
= getSerialFromDir(dir
);
420 shared_ptr
<const SOARecordContent
> soa
;
423 string fname
= workdir
+ "/" + domain
.toString() + "/" + std::to_string(serial
);
424 loadSOAFromDisk(domain
, fname
, soa
, soaTTL
);
426 if (soa
== nullptr) {
427 g_log
<<Logger::Error
<<"Could not load SOA from disk for zone "<<domain
<<", removing file '"<<fname
<<"'"<<endl
;
428 unlink(fname
.c_str());
430 loadZoneFromDisk(records
, fname
, domain
);
431 auto zoneInfo
= std::make_shared
<ixfrinfo_t
>();
432 zoneInfo
->latestAXFR
= std::move(records
);
434 zoneInfo
->soaTTL
= soaTTL
;
435 updateCurrentZoneInfo(domain
, zoneInfo
);
437 if (soa
!= nullptr) {
438 g_log
<<Logger::Notice
<<"Loaded zone "<<domain
<<" with serial "<<soa
->d_st
.serial
<<endl
;
440 cleanUpDomain(domain
, keep
, workdir
);
442 } catch (runtime_error
&e
) {
443 // Most likely, the directory does not exist.
444 g_log
<<Logger::Info
<<e
.what()<<", attempting to create"<<endl
;
445 // Attempt to create it, if _that_ fails, there is no hope
446 if (mkdir(dir
.c_str(), 0777) == -1 && errno
!= EEXIST
) {
447 g_log
<<Logger::Error
<<"Could not create '"<<dir
<<"': "<<stringerror()<<endl
;
453 g_log
<<Logger::Notice
<<"Update Thread started"<<endl
;
457 g_log
<<Logger::Notice
<<"UpdateThread stopped"<<endl
;
460 time_t now
= time(nullptr);
461 for (const auto &domainConfig
: g_domainConfigs
) {
467 ZoneName domain
= domainConfig
.first
;
468 shared_ptr
<const SOARecordContent
> current_soa
;
469 const auto& zoneInfo
= getCurrentZoneInfo(domain
);
470 if (zoneInfo
!= nullptr) {
471 current_soa
= zoneInfo
->soa
;
474 auto& zoneLastCheck
= lastCheck
[domain
];
475 uint32_t refresh
= soaRetry
; // default if we don't get an update at all
476 if (current_soa
!= nullptr) {
477 // Check every `refresh` seconds as advertised in the SOA record
478 refresh
= current_soa
->d_st
.refresh
;
479 if (domainConfig
.second
.maxSOARefresh
> 0) {
480 // Cap refresh value to the configured one if any
481 refresh
= std::min(refresh
, domainConfig
.second
.maxSOARefresh
);
486 if (now
- zoneLastCheck
< refresh
&& g_notifiesReceived
.lock()->erase(domain
) == 0) {
490 // TODO Keep track of 'down' primaries
491 set
<ComboAddress
>::const_iterator
it(domainConfig
.second
.primaries
.begin());
492 std::advance(it
, dns_random(domainConfig
.second
.primaries
.size()));
493 ComboAddress primary
= *it
;
495 string dir
= workdir
+ "/" + domain
.toString();
496 g_log
<< Logger::Info
<< "Attempting to retrieve SOA Serial update for '" << domain
<< "' from '" << primary
.toStringWithPort() << "'" << endl
;
497 shared_ptr
<const SOARecordContent
> sr
;
500 g_stats
.incrementSOAChecks(domain
);
501 auto newSerial
= getSerialFromPrimary(primary
, domain
, sr
); // TODO TSIG
502 if(current_soa
!= nullptr) {
503 g_log
<< Logger::Info
<< "Got SOA Serial for " << domain
<< " from " << primary
.toStringWithPort() << ": " << newSerial
<< ", had Serial: " << current_soa
->d_st
.serial
;
504 if (newSerial
== current_soa
->d_st
.serial
) {
505 g_log
<<Logger::Info
<<", not updating."<<endl
;
508 g_log
<<Logger::Info
<<", will update."<<endl
;
510 } catch (runtime_error
&e
) {
511 g_log
<< Logger::Warning
<< "Unable to get SOA serial update for '" << domain
<< "' from primary " << primary
.toStringWithPort() << ": " << e
.what() << endl
;
512 g_stats
.incrementSOAChecksFailed(domain
);
515 // Now get the full zone!
516 g_log
<<Logger::Info
<<"Attempting to receive full zonedata for '"<<domain
<<"'"<<endl
;
517 ComboAddress local
= primary
.isIPv4() ? ComboAddress("0.0.0.0") : ComboAddress("::");
521 shared_ptr
<const SOARecordContent
> soa
;
525 AXFRRetriever
axfr(primary
, domain
, tt
, &local
);
528 vector
<DNSRecord
> chunk
;
529 time_t t_start
= time(nullptr);
530 time_t axfr_now
= time(nullptr);
531 while(axfr
.getChunk(nop
, &chunk
, (axfr_now
- t_start
+ axfrTimeout
))) {
532 for(auto& dr
: chunk
) {
533 if(dr
.d_type
== QType::TSIG
)
535 if(!dr
.d_name
.isPartOf(domain
)) {
536 throw PDNSException("Out-of-zone data received during AXFR of "+domain
.toLogString());
538 dr
.d_name
.makeUsRelative(domain
);
541 if (dr
.d_type
== QType::SOA
) {
542 soa
= getRR
<SOARecordContent
>(dr
);
546 if (axfrMaxRecords
!= 0 && nrecords
> axfrMaxRecords
) {
547 throw PDNSException("Received more than " + std::to_string(axfrMaxRecords
) + " records in AXFR, aborted");
549 axfr_now
= time(nullptr);
550 if (axfr_now
- t_start
> axfrTimeout
) {
551 g_stats
.incrementAXFRFailures(domain
);
552 throw PDNSException("Total AXFR time exceeded!");
555 if (soa
== nullptr) {
556 g_stats
.incrementAXFRFailures(domain
);
557 g_log
<<Logger::Warning
<<"No SOA was found in the AXFR of "<<domain
<<endl
;
560 g_log
<<Logger::Notice
<<"Retrieved all zone data for "<<domain
<<". Received "<<nrecords
<<" records."<<endl
;
561 } catch (PDNSException
&e
) {
562 g_stats
.incrementAXFRFailures(domain
);
563 g_log
<<Logger::Warning
<<"Could not retrieve AXFR for '"<<domain
<<"': "<<e
.reason
<<endl
;
565 } catch (runtime_error
&e
) {
566 g_stats
.incrementAXFRFailures(domain
);
567 g_log
<<Logger::Warning
<<"Could not retrieve AXFR for zone '"<<domain
<<"': "<<e
.what()<<endl
;
573 writeZoneToDisk(records
, domain
, dir
);
574 g_log
<<Logger::Notice
<<"Wrote zonedata for "<<domain
<<" with serial "<<soa
->d_st
.serial
<<" to "<<dir
<<endl
;
576 const auto oldZoneInfo
= getCurrentZoneInfo(domain
);
577 auto ixfrInfo
= std::make_shared
<ixfrinfo_t
>();
579 if (oldZoneInfo
&& !oldZoneInfo
->latestAXFR
.empty()) {
580 auto diff
= std::make_shared
<ixfrdiff_t
>();
581 ixfrInfo
->ixfrDiffs
= oldZoneInfo
->ixfrDiffs
;
582 g_log
<<Logger::Debug
<<"Calculating diff for "<<domain
<<endl
;
583 makeIXFRDiff(oldZoneInfo
->latestAXFR
, records
, diff
, oldZoneInfo
->soa
, oldZoneInfo
->soaTTL
, soa
, soaTTL
);
584 g_log
<<Logger::Debug
<<"Calculated diff for "<<domain
<<", we had "<<diff
->removals
.size()<<" removals and "<<diff
->additions
.size()<<" additions"<<endl
;
585 ixfrInfo
->ixfrDiffs
.push_back(std::move(diff
));
588 // Clean up the diffs
589 while (ixfrInfo
->ixfrDiffs
.size() > keep
) {
590 ixfrInfo
->ixfrDiffs
.erase(ixfrInfo
->ixfrDiffs
.begin());
593 g_log
<<Logger::Debug
<<"Zone "<<domain
<<" previously contained "<<(oldZoneInfo
? oldZoneInfo
->latestAXFR
.size() : 0)<<" entries, "<<records
.size()<<" now"<<endl
;
594 ixfrInfo
->latestAXFR
= std::move(records
);
595 ixfrInfo
->soa
= std::move(soa
);
596 ixfrInfo
->soaTTL
= soaTTL
;
597 updateCurrentZoneInfo(domain
, ixfrInfo
);
598 } catch (PDNSException
&e
) {
599 g_stats
.incrementAXFRFailures(domain
);
600 g_log
<<Logger::Warning
<<"Could not save zone '"<<domain
<<"' to disk: "<<e
.reason
<<endl
;
601 } catch (runtime_error
&e
) {
602 g_stats
.incrementAXFRFailures(domain
);
603 g_log
<<Logger::Warning
<<"Could not save zone '"<<domain
<<"' to disk: "<<e
.what()<<endl
;
606 // Now clean up the directory
607 cleanUpDomain(domain
, keep
, workdir
);
608 } /* for (const auto &domain : domains) */
613 enum class ResponseType
{
621 static ResponseType
maybeHandleNotify(const MOADNSParser
& mdp
, const ComboAddress
& saddr
, const string
& logPrefix
="") {
622 if (mdp
.d_header
.opcode
!= Opcode::Notify
) { // NOLINT(bugprone-narrowing-conversions, cppcoreguidelines-narrowing-conversions) opcode is 4 bits, this is not a dangerous conversion
623 return ResponseType::Unknown
;
626 g_log
<<Logger::Info
<<logPrefix
<<"NOTIFY for "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).toString()<<" "<< Opcode::to_s(mdp
.d_header
.opcode
) <<" from "<<saddr
.toStringWithPort()<<endl
;
628 ZoneName
zonename(mdp
.d_qname
);
629 auto found
= g_domainConfigs
.find(zonename
);
630 if (found
== g_domainConfigs
.end()) {
631 g_log
<<Logger::Info
<<("Domain name '" + mdp
.d_qname
.toLogString() + "' is not configured for notification")<<endl
;
632 return ResponseType::RefusedQuery
;
635 auto primaries
= found
->second
.primaries
;
637 bool primaryFound
= false;
639 for (const auto& primary
: primaries
) {
640 if (ComboAddress::addressOnlyEqual()(saddr
, primary
)) {
647 g_notifiesReceived
.lock()->insert(zonename
);
649 if (!found
->second
.notify
.empty()) {
650 for (const auto& address
: found
->second
.notify
) {
651 g_log
<< Logger::Debug
<< logPrefix
<< "Queuing notification for " << mdp
.d_qname
<< " to " << address
.toStringWithPort() << std::endl
;
652 g_notificationQueue
.lock()->add(zonename
, address
);
655 return ResponseType::EmptyNoError
;
658 return ResponseType::RefusedQuery
;
661 static ResponseType
checkQuery(const MOADNSParser
& mdp
, const ComboAddress
& saddr
, const bool udp
= true, const string
& logPrefix
="") {
662 vector
<string
> info_msg
;
664 auto ret
= ResponseType::ValidQuery
;
666 g_log
<<Logger::Debug
<<logPrefix
<<"Had "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).toString()<<" query from "<<saddr
.toStringWithPort()<<endl
;
668 if (mdp
.d_header
.opcode
!= Opcode::Query
) { // NOLINT(bugprone-narrowing-conversions, cppcoreguidelines-narrowing-conversions) opcode is 4 bits, this is not a dangerous conversion
669 info_msg
.push_back("Opcode is unsupported (" + Opcode::to_s(mdp
.d_header
.opcode
) + "), expected QUERY"); // note that we also emit this for a NOTIFY from a wrong source
670 ret
= ResponseType::RefusedOpcode
;
673 if (udp
&& mdp
.d_qtype
!= QType::SOA
&& mdp
.d_qtype
!= QType::IXFR
) {
674 info_msg
.push_back("QType is unsupported (" + QType(mdp
.d_qtype
).toString() + " is not in {SOA,IXFR})");
675 ret
= ResponseType::RefusedQuery
;
678 if (!udp
&& mdp
.d_qtype
!= QType::SOA
&& mdp
.d_qtype
!= QType::IXFR
&& mdp
.d_qtype
!= QType::AXFR
) {
679 info_msg
.push_back("QType is unsupported (" + QType(mdp
.d_qtype
).toString() + " is not in {SOA,IXFR,AXFR})");
680 ret
= ResponseType::RefusedQuery
;
684 ZoneName
zonename(mdp
.d_qname
);
685 if (g_domainConfigs
.find(zonename
) == g_domainConfigs
.end()) {
686 info_msg
.push_back("Domain name '" + mdp
.d_qname
.toLogString() + "' is not configured for distribution");
687 ret
= ResponseType::RefusedQuery
;
690 const auto zoneInfo
= getCurrentZoneInfo(zonename
);
691 if (zoneInfo
== nullptr) {
692 info_msg
.emplace_back("Domain has not been transferred yet");
693 ret
= ResponseType::RefusedQuery
;
699 if (!info_msg
.empty()) { // which means ret is not SOA
700 g_log
<<Logger::Warning
<<logPrefix
<<"Refusing "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).toString()<<" "<< Opcode::to_s(mdp
.d_header
.opcode
) <<" from "<<saddr
.toStringWithPort();
701 g_log
<<Logger::Warning
<<": ";
703 for (const auto& s
: info_msg
) {
705 g_log
<<Logger::Warning
<<", ";
708 g_log
<<Logger::Warning
<<s
;
710 g_log
<<Logger::Warning
<<endl
;
711 // fall through to return below
718 * Returns a vector<uint8_t> that represents the full empty NOERROR response.
719 * QNAME is read from mdp.
721 static bool makeEmptyNoErrorPacket(const MOADNSParser
& mdp
, vector
<uint8_t>& packet
) {
722 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
723 pw
.getHeader()->opcode
= mdp
.d_header
.opcode
;
724 pw
.getHeader()->id
= mdp
.d_header
.id
;
725 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
726 pw
.getHeader()->qr
= 1;
727 pw
.getHeader()->aa
= 1;
735 * Returns a vector<uint8_t> that represents the full positive response to a SOA
736 * query. QNAME is read from mdp.
738 static bool makeSOAPacket(const MOADNSParser
& mdp
, vector
<uint8_t>& packet
) {
740 auto zoneInfo
= getCurrentZoneInfo(ZoneName(mdp
.d_qname
));
741 if (zoneInfo
== nullptr) {
745 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
746 pw
.getHeader()->opcode
= mdp
.d_header
.opcode
;
747 pw
.getHeader()->id
= mdp
.d_header
.id
;
748 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
749 pw
.getHeader()->qr
= 1;
750 pw
.getHeader()->aa
= 1;
752 pw
.startRecord(mdp
.d_qname
, QType::SOA
, zoneInfo
->soaTTL
);
753 zoneInfo
->soa
->toPacket(pw
);
760 * Returns a vector<uint8_t> that represents the full REFUSED response to a
761 * query. QNAME and type are read from mdp.
763 static bool makeRefusedPacket(const MOADNSParser
& mdp
, vector
<uint8_t>& packet
) {
764 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
765 pw
.getHeader()->opcode
= mdp
.d_header
.opcode
;
766 pw
.getHeader()->id
= mdp
.d_header
.id
;
767 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
768 pw
.getHeader()->qr
= 1;
769 pw
.getHeader()->rcode
= RCode::Refused
;
775 * Returns a vector<uint8_t> that represents the full NOTIMP response to a
776 * query. QNAME and type are read from mdp.
778 static bool makeNotimpPacket(const MOADNSParser
& mdp
, vector
<uint8_t>& packet
) {
779 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
780 pw
.getHeader()->opcode
= mdp
.d_header
.opcode
;
781 pw
.getHeader()->id
= mdp
.d_header
.id
;
782 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
783 pw
.getHeader()->qr
= 1;
784 pw
.getHeader()->rcode
= RCode::NotImp
;
789 static vector
<uint8_t> getSOAPacket(const MOADNSParser
& mdp
, const shared_ptr
<const SOARecordContent
>& soa
, uint32_t soaTTL
) {
790 vector
<uint8_t> packet
;
791 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
792 pw
.getHeader()->id
= mdp
.d_header
.id
;
793 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
794 pw
.getHeader()->qr
= 1;
797 pw
.startRecord(mdp
.d_qname
, QType::SOA
, soaTTL
);
803 static bool sendPacketOverTCP(int fd
, const std::vector
<uint8_t>& packet
)
806 sendBuf
[0]=packet
.size()/256;
807 sendBuf
[1]=packet
.size()%256;
809 writen2(fd
, sendBuf
, 2);
810 writen2(fd
, &packet
[0], packet
.size());
814 static bool addRecordToWriter(DNSPacketWriter
& pw
, const DNSName
& zoneName
, const DNSRecord
& record
, bool compress
)
816 pw
.startRecord(record
.d_name
+ zoneName
, record
.d_type
, record
.d_ttl
, QClass::IN
, DNSResourceRecord::ANSWER
, compress
);
817 record
.getContent()->toPacket(pw
);
818 if (pw
.size() > 16384) {
825 template <typename T
> static bool sendRecordsOverTCP(int fd
, const MOADNSParser
& mdp
, const T
& records
)
827 vector
<uint8_t> packet
;
829 for (auto it
= records
.cbegin(); it
!= records
.cend();) {
830 bool recordsAdded
= false;
832 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
833 pw
.getHeader()->id
= mdp
.d_header
.id
;
834 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
835 pw
.getHeader()->qr
= 1;
837 while (it
!= records
.cend()) {
838 if (it
->d_type
== QType::SOA
) {
843 if (addRecordToWriter(pw
, mdp
.d_qname
, *it
, g_compress
)) {
850 sendPacketOverTCP(fd
, packet
);
852 if (it
== records
.cbegin()) {
853 /* something is wrong */
861 if (it
== records
.cend() && recordsAdded
) {
863 sendPacketOverTCP(fd
, packet
);
871 static bool handleAXFR(int fd
, const MOADNSParser
& mdp
) {
872 /* we get a shared pointer of the zone info that we can't modify, ever.
873 A newer one may arise in the meantime, but this one will stay valid
877 ZoneName
zonename(mdp
.d_qname
);
878 g_stats
.incrementAXFRinQueries(zonename
);
880 auto zoneInfo
= getCurrentZoneInfo(zonename
);
881 if (zoneInfo
== nullptr) {
885 shared_ptr
<const SOARecordContent
> soa
= zoneInfo
->soa
;
886 uint32_t soaTTL
= zoneInfo
->soaTTL
;
887 const records_t
& records
= zoneInfo
->latestAXFR
;
890 const auto soaPacket
= getSOAPacket(mdp
, soa
, soaTTL
);
891 if (!sendPacketOverTCP(fd
, soaPacket
)) {
895 if (!sendRecordsOverTCP(fd
, mdp
, records
)) {
900 if (!sendPacketOverTCP(fd
, soaPacket
)) {
907 /* Produces an IXFR if one can be made according to the rules in RFC 1995 and
908 * creates a SOA or AXFR packet when required by the RFC.
910 static bool handleIXFR(int fd
, const MOADNSParser
& mdp
, const shared_ptr
<const SOARecordContent
>& clientSOA
) {
911 vector
<std::shared_ptr
<ixfrdiff_t
>> toSend
;
913 /* we get a shared pointer of the zone info that we can't modify, ever.
914 A newer one may arise in the meantime, but this one will stay valid
918 ZoneName
zonename(mdp
.d_qname
);
919 g_stats
.incrementIXFRinQueries(zonename
);
921 auto zoneInfo
= getCurrentZoneInfo(zonename
);
922 if (zoneInfo
== nullptr) {
926 uint32_t ourLatestSerial
= zoneInfo
->soa
->d_st
.serial
;
928 if (rfc1982LessThan(ourLatestSerial
, clientSOA
->d_st
.serial
) || ourLatestSerial
== clientSOA
->d_st
.serial
) {
929 /* RFC 1995 Section 2
930 * If an IXFR query with the same or newer version number than that of
931 * the server is received, it is replied to with a single SOA record of
932 * the server's current version.
934 vector
<uint8_t> packet
;
935 bool ret
= makeSOAPacket(mdp
, packet
);
937 sendPacketOverTCP(fd
, packet
);
942 // as we use push_back in the updater, we know the vector is sorted as oldest first
943 bool shouldAdd
= false;
944 // Get all relevant IXFR differences
945 for (const auto& diff
: zoneInfo
->ixfrDiffs
) {
947 toSend
.push_back(diff
);
950 if (diff
->oldSOA
->d_st
.serial
== clientSOA
->d_st
.serial
) {
951 toSend
.push_back(diff
);
952 // Add all consecutive diffs
957 if (toSend
.empty()) {
958 // FIXME: incrementIXFRFallbacks
959 g_log
<<Logger::Warning
<<"No IXFR available from serial "<<clientSOA
->d_st
.serial
<<" for zone "<<mdp
.d_qname
<<", attempting to send AXFR"<<endl
;
960 return handleAXFR(fd
, mdp
);
964 /* An IXFR packet's ANSWER section looks as follows:
965 * SOA latest_serial C
967 First set of changes:
968 * SOA requested_serial A
969 * ... removed records ...
970 * SOA intermediate_serial B
971 * ... added records ...
974 * SOA intermediate_serial B
975 * ... removed records ...
976 * SOA latest_serial C
977 * ... added records ...
979 * SOA latest_serial C
982 const auto latestSOAPacket
= getSOAPacket(mdp
, zoneInfo
->soa
, zoneInfo
->soaTTL
);
983 if (!sendPacketOverTCP(fd
, latestSOAPacket
)) {
987 for (const auto& diff
: toSend
) {
988 const auto newSOAPacket
= getSOAPacket(mdp
, diff
->newSOA
, diff
->newSOATTL
);
989 const auto oldSOAPacket
= getSOAPacket(mdp
, diff
->oldSOA
, diff
->oldSOATTL
);
991 if (!sendPacketOverTCP(fd
, oldSOAPacket
)) {
995 if (!sendRecordsOverTCP(fd
, mdp
, diff
->removals
)) {
999 if (!sendPacketOverTCP(fd
, newSOAPacket
)) {
1003 if (!sendRecordsOverTCP(fd
, mdp
, diff
->additions
)) {
1008 if (!sendPacketOverTCP(fd
, latestSOAPacket
)) {
1015 static bool allowedByACL(const ComboAddress
& addr
, bool forNotify
= false) {
1017 return g_notifySources
.match(addr
);
1020 return g_acl
.match(addr
);
1023 static void handleUDPRequest(int fd
, boost::any
& /*unused*/)
1026 // TODO make the buffer-size configurable
1029 socklen_t fromlen
= sizeof(saddr
);
1030 int res
= recvfrom(fd
, buf
, sizeof(buf
), 0, (struct sockaddr
*) &saddr
, &fromlen
);
1033 g_log
<<Logger::Warning
<<"Got an empty message from "<<saddr
.toStringWithPort()<<endl
;
1038 auto savedErrno
= errno
;
1039 g_log
<<Logger::Warning
<<"Could not read message from "<<saddr
.toStringWithPort()<<": "<<strerror(savedErrno
)<<endl
;
1043 if (saddr
== ComboAddress("0.0.0.0", 0)) {
1044 g_log
<<Logger::Warning
<<"Could not determine source of message"<<endl
;
1048 if (!allowedByACL(saddr
, true) && !allowedByACL(saddr
, false)) {
1049 g_log
<<Logger::Warning
<<"UDP query from "<<saddr
.toString()<<" did not match any valid query or NOTIFY source, dropping"<<endl
;
1053 MOADNSParser
mdp(true, string(&buf
[0], static_cast<size_t>(res
)));
1054 vector
<uint8_t> packet
;
1056 ResponseType respt
= ResponseType::Unknown
;
1058 if (allowedByACL(saddr
, true)) {
1059 respt
= maybeHandleNotify(mdp
, saddr
);
1061 else if (!allowedByACL(saddr
)) {
1062 g_log
<<Logger::Warning
<<"UDP query from "<<saddr
.toString()<<" is not allowed, dropping"<<endl
;
1066 if (respt
== ResponseType::Unknown
) {
1067 // query was not handled yet (so not a valid NOTIFY)
1068 respt
= checkQuery(mdp
, saddr
);
1070 if (respt
== ResponseType::ValidQuery
) {
1071 /* RFC 1995 Section 2
1072 * Transport of a query may be by either UDP or TCP. If an IXFR query
1073 * is via UDP, the IXFR server may attempt to reply using UDP if the
1074 * entire response can be contained in a single DNS packet. If the UDP
1075 * reply does not fit, the query is responded to with a single SOA
1076 * record of the server's current version to inform the client that a
1077 * TCP query should be initiated.
1079 * Let's not complicate this with IXFR over UDP (and looking if we need to truncate etc).
1080 * Just send the current SOA and let the client try over TCP
1082 g_stats
.incrementSOAinQueries(ZoneName(mdp
.d_qname
)); // FIXME: this also counts IXFR queries (but the response is the same as to a SOA query)
1083 makeSOAPacket(mdp
, packet
);
1084 } else if (respt
== ResponseType::EmptyNoError
) {
1085 makeEmptyNoErrorPacket(mdp
, packet
);
1086 } else if (respt
== ResponseType::RefusedQuery
) {
1087 g_stats
.incrementUnknownDomainInQueries(ZoneName(mdp
.d_qname
));
1088 makeRefusedPacket(mdp
, packet
);
1089 } else if (respt
== ResponseType::RefusedOpcode
) {
1090 g_stats
.incrementNotImplemented(mdp
.d_header
.opcode
);
1091 makeNotimpPacket(mdp
, packet
);
1094 if(sendto(fd
, &packet
[0], packet
.size(), 0, (struct sockaddr
*) &saddr
, fromlen
) < 0) {
1095 auto savedErrno
= errno
;
1096 g_log
<<Logger::Warning
<<"Could not send reply for "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).toString()<<" to "<<saddr
.toStringWithPort()<<": "<<strerror(savedErrno
)<<endl
;
1100 catch(std::exception
& e
) {
1105 static void handleTCPRequest(int fd
, boost::any
&) {
1110 cfd
= SAccept(fd
, saddr
);
1112 } catch(runtime_error
&e
) {
1113 g_log
<<Logger::Error
<<e
.what()<<endl
;
1117 if (saddr
== ComboAddress("0.0.0.0", 0)) {
1118 g_log
<<Logger::Warning
<<"Could not determine source of message"<<endl
;
1123 // we allow the connection if this is a legit client or a legit NOTIFY source
1124 // need to check per-operation later
1125 if (!allowedByACL(saddr
) && !allowedByACL(saddr
, true)) {
1126 g_log
<<Logger::Warning
<<"TCP query from "<<saddr
.toString()<<" is not allowed, dropping"<<endl
;
1132 std::lock_guard
<std::mutex
> lg(g_tcpRequestFDsMutex
);
1133 g_tcpRequestFDs
.push({cfd
, saddr
});
1135 g_tcpHandlerCV
.notify_one();
1138 /* Thread to handle TCP traffic
1140 static void tcpWorker(int tid
) {
1141 setThreadName("ixfrdist/tcpWor");
1142 string prefix
= "TCP Worker " + std::to_string(tid
) + ": ";
1145 g_log
<<Logger::Debug
<<prefix
<<"ready for a new request!"<<endl
;
1146 std::unique_lock
<std::mutex
> lk(g_tcpRequestFDsMutex
);
1147 g_tcpHandlerCV
.wait(lk
, []{return g_tcpRequestFDs
.size() || g_exiting
;});
1149 g_log
<<Logger::Debug
<<prefix
<<"Stopping thread"<<endl
;
1152 g_log
<<Logger::Debug
<<prefix
<<"Going to handle a query"<<endl
;
1153 auto request
= g_tcpRequestFDs
.front();
1154 g_tcpRequestFDs
.pop();
1157 int cfd
= request
.first
;
1158 ComboAddress saddr
= request
.second
;
1164 readn2(cfd
, &toRead
, sizeof(toRead
));
1165 toRead
= std::min(ntohs(toRead
), static_cast<uint16_t>(sizeof(buf
)));
1166 res
= readn2WithTimeout(cfd
, &buf
, toRead
, timeval
{2,0});
1167 g_log
<<Logger::Debug
<<prefix
<<"Had message of "<<std::to_string(toRead
)<<" bytes from "<<saddr
.toStringWithPort()<<endl
;
1168 } catch (runtime_error
&e
) {
1169 g_log
<<Logger::Warning
<<prefix
<<"Could not read message from "<<saddr
.toStringWithPort()<<": "<<e
.what()<<endl
;
1175 MOADNSParser
mdp(true, string(buf
, res
));
1177 ResponseType respt
= ResponseType::Unknown
;
1179 // this code is duplicated from the UDP path
1180 if (allowedByACL(saddr
, true)) {
1181 respt
= maybeHandleNotify(mdp
, saddr
);
1183 else if (!allowedByACL(saddr
)) {
1188 if (respt
== ResponseType::Unknown
) {
1189 respt
= checkQuery(mdp
, saddr
, false, prefix
);
1192 if (respt
!= ResponseType::ValidQuery
&& respt
!= ResponseType::EmptyNoError
) { // on TCP, we currently do not bother with sending useful errors
1197 vector
<uint8_t> packet
;
1199 if (respt
== ResponseType::EmptyNoError
) {
1200 bool ret
= makeEmptyNoErrorPacket(mdp
, packet
);
1205 sendPacketOverTCP(cfd
, packet
);
1207 else if (mdp
.d_qtype
== QType::SOA
) {
1208 bool ret
= makeSOAPacket(mdp
, packet
);
1213 sendPacketOverTCP(cfd
, packet
);
1215 else if (mdp
.d_qtype
== QType::AXFR
) {
1216 if (!handleAXFR(cfd
, mdp
)) {
1221 else if (mdp
.d_qtype
== QType::IXFR
) {
1222 /* RFC 1995 section 3:
1223 * The IXFR query packet format is the same as that of a normal DNS
1224 * query, but with the query type being IXFR and the authority section
1225 * containing the SOA record of client's version of the zone.
1227 shared_ptr
<const SOARecordContent
> clientSOA
;
1228 for (auto &answer
: mdp
.d_answers
) {
1229 // from dnsparser.hh:
1230 // typedef vector<pair<DNSRecord, uint16_t > > answers_t;
1231 if (answer
.d_type
== QType::SOA
&& answer
.d_place
== DNSResourceRecord::AUTHORITY
) {
1232 clientSOA
= getRR
<SOARecordContent
>(answer
);
1233 if (clientSOA
!= nullptr) {
1237 } /* for (auto const &answer : mdp.d_answers) */
1239 if (clientSOA
== nullptr) {
1240 g_log
<<Logger::Warning
<<prefix
<<"IXFR request packet did not contain a SOA record in the AUTHORITY section"<<endl
;
1245 if (!handleIXFR(cfd
, mdp
, clientSOA
)) {
1249 } /* if (mdp.d_qtype == QType::IXFR) */
1252 } catch (const MOADNSException
&mde
) {
1253 g_log
<<Logger::Warning
<<prefix
<<"Could not parse DNS packet from "<<saddr
.toStringWithPort()<<": "<<mde
.what()<<endl
;
1254 } catch (runtime_error
&e
) {
1255 g_log
<<Logger::Warning
<<prefix
<<"Could not write reply to "<<saddr
.toStringWithPort()<<": "<<e
.what()<<endl
;
1266 /* Parses the configuration file in configpath into config, adding defaults for
1267 * missing parameters (if applicable), returning true if the config file was
1268 * good, false otherwise. Will log all issues with the config
1270 static bool parseAndCheckConfig(const string
& configpath
, YAML::Node
& config
) {
1271 g_log
<<Logger::Info
<<"Loading configuration file from "<<configpath
<<endl
;
1273 config
= YAML::LoadFile(configpath
);
1274 } catch (const runtime_error
&e
) {
1275 g_log
<<Logger::Error
<<"Unable to load configuration file '"<<configpath
<<"': "<<e
.what()<<endl
;
1281 if (config
["keep"]) {
1283 config
["keep"].as
<uint16_t>();
1284 } catch (const runtime_error
&e
) {
1285 g_log
<<Logger::Error
<<"Unable to read 'keep' value: "<<e
.what()<<endl
;
1289 config
["keep"] = 20;
1292 if (config
["axfr-max-records"]) {
1294 config
["axfr-max-records"].as
<uint32_t>();
1295 } catch (const runtime_error
&e
) {
1296 g_log
<<Logger::Error
<<"Unable to read 'axfr-max-records' value: "<<e
.what()<<endl
;
1299 config
["axfr-max-records"] = 0;
1302 if (config
["axfr-timeout"]) {
1304 config
["axfr-timeout"].as
<uint16_t>();
1305 } catch (const runtime_error
&e
) {
1306 g_log
<<Logger::Error
<<"Unable to read 'axfr-timeout' value: "<<e
.what()<<endl
;
1309 config
["axfr-timeout"] = 20;
1312 if (config
["failed-soa-retry"]) {
1314 config
["failed-soa-retry"].as
<uint16_t>();
1315 } catch (const runtime_error
&e
) {
1316 g_log
<<Logger::Error
<<"Unable to read 'failed-soa-retry' value: "<<e
.what()<<endl
;
1319 config
["failed-soa-retry"] = 30;
1322 if (config
["tcp-in-threads"]) {
1324 config
["tcp-in-threads"].as
<uint16_t>();
1325 } catch (const runtime_error
&e
) {
1326 g_log
<<Logger::Error
<<"Unable to read 'tcp-in-threads' value: "<<e
.what()<<endl
;
1329 config
["tcp-in-threads"] = 10;
1332 if (config
["listen"]) {
1334 config
["listen"].as
<vector
<ComboAddress
>>();
1335 } catch (const runtime_error
&e
) {
1336 g_log
<<Logger::Error
<<"Unable to read 'listen' value: "<<e
.what()<<endl
;
1340 config
["listen"].push_back("127.0.0.1:53");
1341 config
["listen"].push_back("[::1]:53");
1344 if (config
["acl"]) {
1346 config
["acl"].as
<vector
<string
>>();
1347 } catch (const runtime_error
&e
) {
1348 g_log
<<Logger::Error
<<"Unable to read 'acl' value: "<<e
.what()<<endl
;
1352 config
["acl"].push_back("127.0.0.0/8");
1353 config
["acl"].push_back("::1/128");
1356 if (config
["work-dir"]) {
1358 config
["work-dir"].as
<string
>();
1359 } catch(const runtime_error
&e
) {
1360 g_log
<<Logger::Error
<<"Unable to read 'work-dir' value: "<<e
.what()<<endl
;
1365 config
["work-dir"] = getcwd(tmp
, sizeof(tmp
)) ? string(tmp
) : "";;
1368 if (config
["uid"]) {
1370 config
["uid"].as
<string
>();
1371 } catch(const runtime_error
&e
) {
1372 g_log
<<Logger::Error
<<"Unable to read 'uid' value: "<<e
.what()<<endl
;
1377 if (config
["gid"]) {
1379 config
["gid"].as
<string
>();
1380 } catch(const runtime_error
&e
) {
1381 g_log
<<Logger::Error
<<"Unable to read 'gid' value: "<<e
.what()<<endl
;
1386 if (config
["domains"]) {
1387 if (config
["domains"].size() == 0) {
1388 g_log
<<Logger::Error
<<"No domains configured"<<endl
;
1391 for (auto const &domain
: config
["domains"]) {
1393 if (!domain
["domain"]) {
1394 g_log
<<Logger::Error
<<"An entry in 'domains' is missing a 'domain' key!"<<endl
;
1398 domain
["domain"].as
<ZoneName
>();
1399 } catch (const runtime_error
&e
) {
1400 g_log
<<Logger::Error
<<"Unable to read domain '"<<domain
["domain"].as
<string
>()<<"': "<<e
.what()<<endl
;
1403 if (!domain
["master"]) {
1404 g_log
<< Logger::Error
<< "Domain '" << domain
["domain"].as
<string
>() << "' has no primary configured!" << endl
;
1408 domain
["master"].as
<ComboAddress
>();
1410 auto notifySource
= domain
["master"].as
<ComboAddress
>();
1412 g_notifySources
.addMask(notifySource
);
1413 } catch (const runtime_error
&e
) {
1414 g_log
<< Logger::Error
<< "Unable to read domain '" << domain
["domain"].as
<string
>() << "' primary address: " << e
.what() << endl
;
1417 if (domain
["max-soa-refresh"]) {
1419 domain
["max-soa-refresh"].as
<uint32_t>();
1420 } catch (const runtime_error
&e
) {
1421 g_log
<<Logger::Error
<<"Unable to read 'max-soa-refresh' value for domain '"<<domain
["domain"].as
<string
>()<<"': "<<e
.what()<<endl
;
1426 g_log
<<Logger::Error
<<"No domains configured"<<endl
;
1430 if (config
["compress"]) {
1432 config
["compress"].as
<bool>();
1434 catch (const runtime_error
&e
) {
1435 g_log
<<Logger::Error
<<"Unable to read 'compress' value: "<<e
.what()<<endl
;
1440 config
["compress"] = false;
1443 if (config
["webserver-address"]) {
1445 config
["webserver-address"].as
<ComboAddress
>();
1447 catch (const runtime_error
&e
) {
1448 g_log
<<Logger::Error
<<"Unable to read 'webserver-address' value: "<<e
.what()<<endl
;
1453 if (config
["webserver-acl"]) {
1455 config
["webserver-acl"].as
<vector
<Netmask
>>();
1457 catch (const runtime_error
&e
) {
1458 g_log
<<Logger::Error
<<"Unable to read 'webserver-acl' value: "<<e
.what()<<endl
;
1463 if (config
["webserver-loglevel"]) {
1465 config
["webserver-loglevel"].as
<string
>();
1467 catch (const runtime_error
&e
) {
1468 g_log
<<Logger::Error
<<"Unable to read 'webserver-loglevel' value: "<<e
.what()<<endl
;
1476 struct IXFRDistConfiguration
1478 set
<int> listeningSockets
;
1480 ComboAddress wsAddr
;
1481 std::string wsLogLevel
{"normal"};
1482 std::string workDir
;
1483 const struct passwd
* userInfo
{nullptr};
1484 uint32_t axfrMaxRecords
{0};
1486 uint16_t axfrTimeout
{0};
1487 uint16_t failedSOARetry
{0};
1488 uint16_t tcpInThreads
{0};
1491 bool shouldExit
{false};
1494 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
1495 static std::optional
<IXFRDistConfiguration
> parseConfiguration(int argc
, char** argv
, FDMultiplexer
& fdm
)
1497 IXFRDistConfiguration configuration
;
1498 po::variables_map g_vm
;
1499 std::string configPath
;
1502 po::options_description
desc("IXFR distribution tool");
1504 ("help", "produce help message")
1505 ("version", "Display the version of ixfrdist")
1506 ("verbose", "Be verbose")
1507 ("debug", "Be even more verbose")
1508 ("config", po::value
<string
>()->default_value(SYSCONFDIR
+ string("/ixfrdist.yml")), "Configuration file to use")
1511 po::store(po::command_line_parser(argc
, argv
).options(desc
).run(), g_vm
);
1514 if (g_vm
.count("help") > 0) {
1516 configuration
.shouldExit
= true;
1517 return configuration
;
1520 if (g_vm
.count("version") > 0) {
1521 cout
<<"ixfrdist "<<VERSION
<<endl
;
1522 configuration
.shouldExit
= true;
1523 return configuration
;
1526 configPath
= g_vm
["config"].as
<string
>();
1528 catch (const po::error
&e
) {
1529 g_log
<<Logger::Error
<<e
.what()<<". See `ixfrdist --help` for valid options"<<endl
;
1530 return std::nullopt
;
1532 catch (const std::exception
& exp
) {
1533 g_log
<<Logger::Error
<<exp
.what()<<". See `ixfrdist --help` for valid options"<<endl
;
1534 return std::nullopt
;
1537 bool had_error
= false;
1539 if (g_vm
.count("verbose")) {
1540 g_log
.setLoglevel(Logger::Info
);
1541 g_log
.toConsole(Logger::Info
);
1544 if (g_vm
.count("debug") > 0) {
1545 g_log
.setLoglevel(Logger::Debug
);
1546 g_log
.toConsole(Logger::Debug
);
1549 g_log
<<Logger::Notice
<<"IXFR distributor version "<<VERSION
<<" starting up!"<<endl
;
1553 if (!parseAndCheckConfig(configPath
, config
)) {
1554 // parseAndCheckConfig already logged whatever was wrong
1555 return std::nullopt
;
1558 /* From hereon out, we known that all the values in config are valid. */
1560 for (auto const &domain
: config
["domains"]) {
1561 set
<ComboAddress
> s
;
1562 s
.insert(domain
["master"].as
<ComboAddress
>());
1563 g_domainConfigs
[domain
["domain"].as
<ZoneName
>()].primaries
= s
;
1564 if (domain
["max-soa-refresh"].IsDefined()) {
1565 g_domainConfigs
[domain
["domain"].as
<ZoneName
>()].maxSOARefresh
= domain
["max-soa-refresh"].as
<uint32_t>();
1567 if (domain
["notify"].IsDefined()) {
1568 auto& listset
= g_domainConfigs
[domain
["domain"].as
<ZoneName
>()].notify
;
1569 if (domain
["notify"].IsScalar()) {
1570 auto remote
= domain
["notify"].as
<std::string
>();
1572 listset
.emplace(remote
, 53);
1574 catch (PDNSException
& e
) {
1575 g_log
<< Logger::Error
<< "Unparseable IP in notify directive " << remote
<< ". Error: " << e
.reason
<< endl
;
1577 } else if (domain
["notify"].IsSequence()) {
1578 for (const auto& entry
: domain
["notify"]) {
1579 auto remote
= entry
.as
<std::string
>();
1581 listset
.emplace(remote
, 53);
1583 catch (PDNSException
& e
) {
1584 g_log
<< Logger::Error
<< "Unparseable IP in notify directive " << remote
<< ". Error: " << e
.reason
<< endl
;
1589 g_stats
.registerDomain(domain
["domain"].as
<ZoneName
>());
1592 for (const auto &addr
: config
["acl"].as
<vector
<string
>>()) {
1594 g_acl
.addMask(addr
);
1596 catch (const std::exception
& exp
) {
1597 g_log
<<Logger::Error
<<exp
.what()<<endl
;
1600 catch (const NetmaskException
&e
) {
1601 g_log
<<Logger::Error
<<e
.reason
<<endl
;
1607 g_log
<<Logger::Notice
<<"ACL set to "<<g_acl
.toString()<<"."<<endl
;
1609 catch (const std::exception
& exp
) {
1610 g_log
<<Logger::Error
<<"Error printing ACL: "<<exp
.what()<<endl
;
1613 g_log
<<Logger::Notice
<<"NOTIFY accepted from "<<g_notifySources
.toString()<<"."<<endl
;
1615 if (config
["compress"].IsDefined()) {
1616 g_compress
= config
["compress"].as
<bool>();
1618 g_log
<<Logger::Notice
<<"Record compression is enabled."<<endl
;
1622 for (const auto& addr
: config
["listen"].as
<vector
<ComboAddress
>>()) {
1623 for (const auto& stype
: {SOCK_DGRAM
, SOCK_STREAM
}) {
1625 int s
= SSocket(addr
.sin4
.sin_family
, stype
, 0);
1628 if (addr
.isIPv6()) {
1630 (void)setsockopt(s
, IPPROTO_IPV6
, IPV6_V6ONLY
, &one
, sizeof(one
));
1634 if (stype
== SOCK_STREAM
) {
1635 SListen(s
, 30); // TODO make this configurable
1637 fdm
.addReadFD(s
, stype
== SOCK_DGRAM
? handleUDPRequest
: handleTCPRequest
);
1638 configuration
.listeningSockets
.insert(s
);
1640 catch (const runtime_error
& exp
) {
1641 g_log
<<Logger::Error
<<exp
.what()<<endl
;
1645 catch (const PDNSException
& exp
) {
1646 g_log
<<Logger::Error
<<exp
.reason
<<endl
;
1653 if (config
["gid"].IsDefined()) {
1654 bool gidParsed
= false;
1655 auto gid
= config
["gid"].as
<string
>();
1657 configuration
.gid
= pdns::checked_stoi
<gid_t
>(gid
);
1660 catch (const std::exception
& e
) {
1661 configuration
.gid
= 0;
1664 //NOLINTNEXTLINE(concurrency-mt-unsafe): only one thread at this point
1665 const struct group
*gr
= getgrnam(gid
.c_str());
1666 if (gr
== nullptr) {
1667 g_log
<<Logger::Error
<<"Can not determine group-id for gid "<<gid
<<endl
;
1670 configuration
.gid
= gr
->gr_gid
;
1675 if (config
["webserver-address"].IsDefined()) {
1676 configuration
.wsAddr
= config
["webserver-address"].as
<ComboAddress
>();
1679 configuration
.wsACL
.addMask("127.0.0.0/8");
1680 configuration
.wsACL
.addMask("::1/128");
1682 if (config
["webserver-acl"].IsDefined()) {
1683 configuration
.wsACL
.clear();
1684 for (const auto &acl
: config
["webserver-acl"].as
<vector
<Netmask
>>()) {
1685 configuration
.wsACL
.addMask(acl
);
1689 catch (const NetmaskException
& ne
) {
1690 g_log
<<Logger::Error
<<"Could not set the webserver ACL: "<<ne
.reason
<<endl
;
1693 catch (const std::exception
& exp
) {
1694 g_log
<<Logger::Error
<<"Could not set the webserver ACL: "<<exp
.what()<<endl
;
1698 if (config
["webserver-loglevel"]) {
1699 configuration
.wsLogLevel
= config
["webserver-loglevel"].as
<string
>();
1703 if (config
["uid"].IsDefined()) {
1704 bool uidParsed
= false;
1705 auto uid
= config
["uid"].as
<string
>();
1707 configuration
.uid
= pdns::checked_stoi
<uid_t
>(uid
);
1710 catch (const std::exception
& e
) {
1711 configuration
.uid
= 0;
1714 //NOLINTNEXTLINE(concurrency-mt-unsafe): only one thread at this point
1715 const struct passwd
*pw
= getpwnam(uid
.c_str());
1716 if (pw
== nullptr) {
1717 g_log
<<Logger::Error
<<"Can not determine user-id for uid "<<uid
<<endl
;
1720 configuration
.uid
= pw
->pw_uid
;
1723 //NOLINTNEXTLINE(concurrency-mt-unsafe): only one thread at this point
1726 configuration
.userInfo
= getpwuid(configuration
.uid
);
1730 configuration
.workDir
= config
["work-dir"].as
<string
>();
1731 configuration
.keep
= config
["keep"].as
<uint16_t>();
1732 configuration
.axfrTimeout
= config
["axfr-timeout"].as
<uint16_t>();
1733 configuration
.failedSOARetry
= config
["failed-soa-retry"].as
<uint16_t>();
1734 configuration
.axfrMaxRecords
= config
["axfr-max-records"].as
<uint32_t>();
1735 configuration
.tcpInThreads
= config
["tcp-in-threads"].as
<uint16_t>();
1738 return std::nullopt
;
1740 return configuration
;
1742 catch (const YAML::Exception
& exp
) {
1744 g_log
<<Logger::Error
<<"Got an exception while applying our configuration: "<<exp
.msg
<<endl
;
1745 return std::nullopt
;
1749 int main(int argc
, char** argv
) {
1750 bool had_error
= false;
1751 std::optional
<IXFRDistConfiguration
> configuration
{std::nullopt
};
1752 std::unique_ptr
<FDMultiplexer
> fdm
{nullptr};
1755 g_log
.setLoglevel(Logger::Notice
);
1756 g_log
.toConsole(Logger::Notice
);
1757 g_log
.setPrefixed(true);
1758 g_log
.disableSyslog(true);
1759 g_log
.setTimestamps(false);
1761 fdm
= std::unique_ptr
<FDMultiplexer
>(FDMultiplexer::getMultiplexerSilent());
1763 g_log
<<Logger::Error
<<"Could not enable a multiplexer for the listen sockets!"<<endl
;
1764 return EXIT_FAILURE
;
1767 configuration
= parseConfiguration(argc
, argv
, *fdm
);
1768 if (!configuration
) {
1769 // We have already sent the errors to stderr, just die
1770 return EXIT_FAILURE
;
1773 if (configuration
->shouldExit
) {
1774 return EXIT_SUCCESS
;
1777 catch (const YAML::Exception
& exp
) {
1779 g_log
<<Logger::Error
<<"Got an exception while processing our configuration: "<<exp
.msg
<<endl
;
1783 if (configuration
->gid
!= 0) {
1784 g_log
<<Logger::Notice
<<"Dropping effective group-id to "<<configuration
->gid
<<endl
;
1785 if (setgid(configuration
->gid
) < 0) {
1786 g_log
<<Logger::Error
<<"Could not set group id to "<<configuration
->gid
<<": "<<stringerror()<<endl
;
1791 // It all starts here
1792 signal(SIGTERM
, handleSignal
);
1793 signal(SIGINT
, handleSignal
);
1794 //NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
1795 signal(SIGPIPE
, SIG_IGN
);
1797 // Launch the webserver!
1799 std::thread(&IXFRDistWebServer::go
, IXFRDistWebServer(configuration
->wsAddr
, configuration
->wsACL
, configuration
->wsLogLevel
)).detach();
1801 catch (const std::exception
& exp
) {
1802 g_log
<<Logger::Error
<<"Unable to start webserver: "<<exp
.what()<<endl
;
1805 catch (const PDNSException
&e
) {
1806 g_log
<<Logger::Error
<<"Unable to start webserver: "<<e
.reason
<<endl
;
1810 if (configuration
->uid
!= 0) {
1811 if (configuration
->userInfo
== nullptr) {
1812 if (setgroups(0, nullptr) < 0) {
1813 g_log
<<Logger::Error
<<"Unable to drop supplementary gids: "<<stringerror()<<endl
;
1817 if (initgroups(configuration
->userInfo
->pw_name
, configuration
->gid
) < 0) {
1818 g_log
<<Logger::Error
<<"Unable to set supplementary groups: "<<stringerror()<<endl
;
1823 g_log
<<Logger::Notice
<<"Dropping effective user-id to "<<configuration
->uid
<<endl
;
1824 if (setuid(configuration
->uid
) < 0) {
1825 g_log
<<Logger::Error
<<"Could not set user id to "<<configuration
->uid
<<": "<<stringerror()<<endl
;
1831 return EXIT_FAILURE
;
1834 catch (const YAML::Exception
& exp
) {
1836 g_log
<<Logger::Error
<<"Got an exception while applying our configuration: "<<exp
.msg
<<endl
;
1840 // Init the things we need
1843 std::thread
ut(updateThread
,
1844 configuration
->workDir
,
1845 configuration
->keep
,
1846 configuration
->axfrTimeout
,
1847 configuration
->failedSOARetry
,
1848 configuration
->axfrMaxRecords
);
1849 std::thread
communicator(communicatorThread
);
1851 vector
<std::thread
> tcpHandlers
;
1852 tcpHandlers
.reserve(configuration
->tcpInThreads
);
1853 for (size_t i
= 0; i
< tcpHandlers
.capacity(); ++i
) {
1854 tcpHandlers
.push_back(std::thread(tcpWorker
, i
));
1859 gettimeofday(&now
, 0);
1862 g_log
<<Logger::Debug
<<"Closing listening sockets"<<endl
;
1863 for (const int& fd
: configuration
->listeningSockets
) {
1866 } catch (const PDNSException
&e
) {
1867 g_log
<<Logger::Error
<<e
.reason
<<endl
;
1874 g_log
<<Logger::Debug
<<"Waiting for all threads to stop"<<endl
;
1875 g_tcpHandlerCV
.notify_all();
1877 communicator
.join();
1878 for (auto &t
: tcpHandlers
) {
1881 g_log
<<Logger::Notice
<<"IXFR distributor stopped"<<endl
;
1883 catch (const YAML::Exception
& exp
) {
1885 g_log
<<Logger::Error
<<"Got an exception: "<<exp
.msg
<<endl
;
1888 return had_error
? EXIT_FAILURE
: EXIT_SUCCESS
;