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.
25 #include <boost/program_options.hpp>
26 #include <arpa/inet.h>
27 #include <sys/types.h>
33 #include "threadname.hh"
36 #include <condition_variable>
38 #include "ixfrutils.hh"
39 #include "resolver.hh"
40 #include "dns_random.hh"
46 #include "ixfrdist-stats.hh"
47 #include "ixfrdist-web.hh"
48 #include <yaml-cpp/yaml.h>
50 /* BEGIN Needed because of deeper dependencies */
51 #include "arguments.hh"
57 static ArgvMap theArg
;
60 /* END Needed because of deeper dependencies */
62 // Allows reading/writing ComboAddresses and DNSNames in YAML-cpp
65 struct convert
<ComboAddress
> {
66 static Node
encode(const ComboAddress
& rhs
) {
67 return Node(rhs
.toStringWithPort());
69 static bool decode(const Node
& node
, ComboAddress
& rhs
) {
70 if (!node
.IsScalar()) {
74 rhs
= ComboAddress(node
.as
<string
>(), 53);
76 } catch(const runtime_error
&e
) {
78 } catch (const PDNSException
&e
) {
85 struct convert
<DNSName
> {
86 static Node
encode(const DNSName
& rhs
) {
87 return Node(rhs
.toStringRootDot());
89 static bool decode(const Node
& node
, DNSName
& rhs
) {
90 if (!node
.IsScalar()) {
94 rhs
= DNSName(node
.as
<string
>());
96 } catch(const runtime_error
&e
) {
98 } catch (const PDNSException
&e
) {
105 struct convert
<Netmask
> {
106 static Node
encode(const Netmask
& rhs
) {
107 return Node(rhs
.toString());
109 static bool decode(const Node
& node
, Netmask
& rhs
) {
110 if (!node
.IsScalar()) {
114 rhs
= Netmask(node
.as
<string
>());
116 } catch(const runtime_error
&e
) {
118 } catch (const PDNSException
&e
) {
126 shared_ptr
<SOARecordContent
> oldSOA
;
127 shared_ptr
<SOARecordContent
> newSOA
;
128 vector
<DNSRecord
> removals
;
129 vector
<DNSRecord
> additions
;
135 shared_ptr
<SOARecordContent
> soa
; // The SOA of the latest AXFR
136 records_t latestAXFR
; // The most recent AXFR
137 vector
<std::shared_ptr
<ixfrdiff_t
>> ixfrDiffs
;
141 // Why a struct? This way we can add more options to a domain in the future
142 struct ixfrdistdomain_t
{
143 set
<ComboAddress
> masters
; // A set so we can do multiple master addresses in the future
146 // This contains the configuration for each domain
147 static map
<DNSName
, ixfrdistdomain_t
> g_domainConfigs
;
149 // Map domains and their data
150 static std::map
<DNSName
, std::shared_ptr
<ixfrinfo_t
>> g_soas
;
151 static std::mutex g_soas_mutex
;
153 // Condition variable for TCP handling
154 static std::condition_variable g_tcpHandlerCV
;
155 static std::queue
<pair
<int, ComboAddress
>> g_tcpRequestFDs
;
156 static std::mutex g_tcpRequestFDsMutex
;
158 namespace po
= boost::program_options
;
160 static bool g_exiting
= false;
162 static NetmaskGroup g_acl
;
163 static bool g_compress
= false;
165 static ixfrdistStats g_stats
;
167 // g_stats is static, so local to this file. But the webserver needs this info
168 string
doGetStats() {
169 return g_stats
.getStats();
172 static void handleSignal(int signum
) {
173 g_log
<<Logger::Notice
<<"Got "<<strsignal(signum
)<<" signal";
175 g_log
<<Logger::Notice
<<", this is the second time we were asked to stop, forcefully exiting"<<endl
;
178 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
;
182 static void usage(po::options_description
&desc
) {
183 cerr
<< "Usage: ixfrdist [OPTION]..."<<endl
;
184 cerr
<< desc
<< "\n";
187 // The compiler does not like using rfc1982LessThan in std::sort directly
188 static bool sortSOA(uint32_t i
, uint32_t j
) {
189 return rfc1982LessThan(i
, j
);
192 static void cleanUpDomain(const DNSName
& domain
, const uint16_t& keep
, const string
& workdir
) {
193 string dir
= workdir
+ "/" + domain
.toString();
195 dp
= opendir(dir
.c_str());
199 vector
<uint32_t> zoneVersions
;
201 while ((d
= readdir(dp
)) != nullptr) {
202 if(!strcmp(d
->d_name
, ".") || !strcmp(d
->d_name
, "..")) {
205 zoneVersions
.push_back(std::stoi(d
->d_name
));
208 g_log
<<Logger::Info
<<"Found "<<zoneVersions
.size()<<" versions of "<<domain
<<", asked to keep "<<keep
<<", ";
209 if (zoneVersions
.size() <= keep
) {
210 g_log
<<Logger::Info
<<"not cleaning up"<<endl
;
213 g_log
<<Logger::Info
<<"cleaning up the oldest "<<zoneVersions
.size() - keep
<<endl
;
216 std::sort(zoneVersions
.begin(), zoneVersions
.end(), sortSOA
);
218 // And delete all the old ones
220 // Lock to ensure no one reads this.
221 std::lock_guard
<std::mutex
> guard(g_soas_mutex
);
222 for (auto iter
= zoneVersions
.cbegin(); iter
!= zoneVersions
.cend() - keep
; ++iter
) {
223 string fname
= dir
+ "/" + std::to_string(*iter
);
224 g_log
<<Logger::Debug
<<"Removing "<<fname
<<endl
;
225 unlink(fname
.c_str());
230 static void getSOAFromRecords(const records_t
& records
, shared_ptr
<SOARecordContent
>& soa
, uint32_t& soaTTL
) {
231 for (const auto& dnsrecord
: records
) {
232 if (dnsrecord
.d_type
== QType::SOA
) {
233 soa
= getRR
<SOARecordContent
>(dnsrecord
);
234 if (soa
== nullptr) {
235 throw PDNSException("Unable to determine SOARecordContent from old records");
237 soaTTL
= dnsrecord
.d_ttl
;
241 throw PDNSException("No SOA in supplied records");
244 static void makeIXFRDiff(const records_t
& from
, const records_t
& to
, std::shared_ptr
<ixfrdiff_t
>& diff
, const shared_ptr
<SOARecordContent
>& fromSOA
= nullptr, uint32_t fromSOATTL
=0, const shared_ptr
<SOARecordContent
>& toSOA
= nullptr, uint32_t toSOATTL
= 0) {
245 set_difference(from
.cbegin(), from
.cend(), to
.cbegin(), to
.cend(), back_inserter(diff
->removals
), from
.value_comp());
246 set_difference(to
.cbegin(), to
.cend(), from
.cbegin(), from
.cend(), back_inserter(diff
->additions
), from
.value_comp());
247 diff
->oldSOA
= fromSOA
;
248 diff
->oldSOATTL
= fromSOATTL
;
249 if (fromSOA
== nullptr) {
250 getSOAFromRecords(from
, diff
->oldSOA
, diff
->oldSOATTL
);
252 diff
->newSOA
= toSOA
;
253 diff
->newSOATTL
= toSOATTL
;
254 if (toSOA
== nullptr) {
255 getSOAFromRecords(to
, diff
->newSOA
, diff
->newSOATTL
);
259 /* you can _never_ alter the content of the resulting shared pointer */
260 static std::shared_ptr
<ixfrinfo_t
> getCurrentZoneInfo(const DNSName
& domain
)
262 std::lock_guard
<std::mutex
> guard(g_soas_mutex
);
263 return g_soas
[domain
];
266 static void updateCurrentZoneInfo(const DNSName
& domain
, std::shared_ptr
<ixfrinfo_t
>& newInfo
)
268 std::lock_guard
<std::mutex
> guard(g_soas_mutex
);
269 g_soas
[domain
] = newInfo
;
270 g_stats
.setSOASerial(domain
, newInfo
->soa
->d_st
.serial
);
271 // FIXME: also report zone size?
274 void updateThread(const string
& workdir
, const uint16_t& keep
, const uint16_t& axfrTimeout
, const uint16_t& soaRetry
, const uint32_t axfrMaxRecords
) {
275 setThreadName("ixfrdist/update");
276 std::map
<DNSName
, time_t> lastCheck
;
278 // Initialize the serials we have
279 for (const auto &domainConfig
: g_domainConfigs
) {
280 DNSName domain
= domainConfig
.first
;
281 lastCheck
[domain
] = 0;
282 string dir
= workdir
+ "/" + domain
.toString();
284 g_log
<<Logger::Info
<<"Trying to initially load domain "<<domain
<<" from disk"<<endl
;
285 auto serial
= getSerialsFromDir(dir
);
286 shared_ptr
<SOARecordContent
> soa
;
289 string fname
= workdir
+ "/" + domain
.toString() + "/" + std::to_string(serial
);
290 loadSOAFromDisk(domain
, fname
, soa
, soaTTL
);
292 if (soa
!= nullptr) {
293 loadZoneFromDisk(records
, fname
, domain
);
295 auto zoneInfo
= std::make_shared
<ixfrinfo_t
>();
296 zoneInfo
->latestAXFR
= std::move(records
);
298 zoneInfo
->soaTTL
= soaTTL
;
299 updateCurrentZoneInfo(domain
, zoneInfo
);
301 if (soa
!= nullptr) {
302 g_log
<<Logger::Notice
<<"Loaded zone "<<domain
<<" with serial "<<soa
->d_st
.serial
<<endl
;
304 cleanUpDomain(domain
, keep
, workdir
);
306 } catch (runtime_error
&e
) {
307 // Most likely, the directory does not exist.
308 g_log
<<Logger::Info
<<e
.what()<<", attempting to create"<<endl
;
309 // Attempt to create it, if _that_ fails, there is no hope
310 if (mkdir(dir
.c_str(), 0777) == -1 && errno
!= EEXIST
) {
311 g_log
<<Logger::Error
<<"Could not create '"<<dir
<<"': "<<strerror(errno
)<<endl
;
317 g_log
<<Logger::Notice
<<"Update Thread started"<<endl
;
321 g_log
<<Logger::Notice
<<"UpdateThread stopped"<<endl
;
324 time_t now
= time(nullptr);
325 for (const auto &domainConfig
: g_domainConfigs
) {
331 DNSName domain
= domainConfig
.first
;
332 shared_ptr
<SOARecordContent
> current_soa
;
333 const auto& zoneInfo
= getCurrentZoneInfo(domain
);
334 if (zoneInfo
!= nullptr) {
335 current_soa
= zoneInfo
->soa
;
338 auto& zoneLastCheck
= lastCheck
[domain
];
339 if ((current_soa
!= nullptr && now
- zoneLastCheck
< current_soa
->d_st
.refresh
) || // Only check if we have waited `refresh` seconds
340 (current_soa
== nullptr && now
- zoneLastCheck
< soaRetry
)) { // Or if we could not get an update at all still, every 30 seconds
344 // TODO Keep track of 'down' masters
345 set
<ComboAddress
>::const_iterator
it(domainConfig
.second
.masters
.begin());
346 std::advance(it
, dns_random(domainConfig
.second
.masters
.size()));
347 ComboAddress master
= *it
;
349 string dir
= workdir
+ "/" + domain
.toString();
350 g_log
<<Logger::Info
<<"Attempting to retrieve SOA Serial update for '"<<domain
<<"' from '"<<master
.toStringWithPort()<<"'"<<endl
;
351 shared_ptr
<SOARecordContent
> sr
;
354 g_stats
.incrementSOAChecks(domain
);
355 auto newSerial
= getSerialFromMaster(master
, domain
, sr
); // TODO TSIG
356 if(current_soa
!= nullptr) {
357 g_log
<<Logger::Info
<<"Got SOA Serial for "<<domain
<<" from "<<master
.toStringWithPort()<<": "<< newSerial
<<", had Serial: "<<current_soa
->d_st
.serial
;
358 if (newSerial
== current_soa
->d_st
.serial
) {
359 g_log
<<Logger::Info
<<", not updating."<<endl
;
362 g_log
<<Logger::Info
<<", will update."<<endl
;
364 } catch (runtime_error
&e
) {
365 g_log
<<Logger::Warning
<<"Unable to get SOA serial update for '"<<domain
<<"' from master "<<master
.toStringWithPort()<<": "<<e
.what()<<endl
;
366 g_stats
.incrementSOAChecksFailed(domain
);
369 // Now get the full zone!
370 g_log
<<Logger::Info
<<"Attempting to receive full zonedata for '"<<domain
<<"'"<<endl
;
371 ComboAddress local
= master
.isIPv4() ? ComboAddress("0.0.0.0") : ComboAddress("::");
375 shared_ptr
<SOARecordContent
> soa
;
379 AXFRRetriever
axfr(master
, domain
, tt
, &local
);
382 vector
<DNSRecord
> chunk
;
383 time_t t_start
= time(nullptr);
384 time_t axfr_now
= time(nullptr);
385 while(axfr
.getChunk(nop
, &chunk
, (axfr_now
- t_start
+ axfrTimeout
))) {
386 for(auto& dr
: chunk
) {
387 if(dr
.d_type
== QType::TSIG
)
389 if(!dr
.d_name
.isPartOf(domain
)) {
390 throw PDNSException("Out-of-zone data received during AXFR of "+domain
.toLogString());
392 dr
.d_name
.makeUsRelative(domain
);
395 if (dr
.d_type
== QType::SOA
) {
396 soa
= getRR
<SOARecordContent
>(dr
);
400 if (axfrMaxRecords
!= 0 && nrecords
> axfrMaxRecords
) {
401 throw PDNSException("Received more than " + std::to_string(axfrMaxRecords
) + " records in AXFR, aborted");
403 axfr_now
= time(nullptr);
404 if (axfr_now
- t_start
> axfrTimeout
) {
405 g_stats
.incrementAXFRFailures(domain
);
406 throw PDNSException("Total AXFR time exceeded!");
409 if (soa
== nullptr) {
410 g_stats
.incrementAXFRFailures(domain
);
411 g_log
<<Logger::Warning
<<"No SOA was found in the AXFR of "<<domain
<<endl
;
414 g_log
<<Logger::Notice
<<"Retrieved all zone data for "<<domain
<<". Received "<<nrecords
<<" records."<<endl
;
415 } catch (PDNSException
&e
) {
416 g_stats
.incrementAXFRFailures(domain
);
417 g_log
<<Logger::Warning
<<"Could not retrieve AXFR for '"<<domain
<<"': "<<e
.reason
<<endl
;
419 } catch (runtime_error
&e
) {
420 g_stats
.incrementAXFRFailures(domain
);
421 g_log
<<Logger::Warning
<<"Could not retrieve AXFR for zone '"<<domain
<<"': "<<e
.what()<<endl
;
427 writeZoneToDisk(records
, domain
, dir
);
428 g_log
<<Logger::Notice
<<"Wrote zonedata for "<<domain
<<" with serial "<<soa
->d_st
.serial
<<" to "<<dir
<<endl
;
430 const auto oldZoneInfo
= getCurrentZoneInfo(domain
);
431 auto zoneInfo
= std::make_shared
<ixfrinfo_t
>();
433 if (oldZoneInfo
&& !oldZoneInfo
->latestAXFR
.empty()) {
434 auto diff
= std::make_shared
<ixfrdiff_t
>();
435 zoneInfo
->ixfrDiffs
= oldZoneInfo
->ixfrDiffs
;
436 g_log
<<Logger::Debug
<<"Calculating diff for "<<domain
<<endl
;
437 makeIXFRDiff(oldZoneInfo
->latestAXFR
, records
, diff
, oldZoneInfo
->soa
, oldZoneInfo
->soaTTL
, soa
, soaTTL
);
438 g_log
<<Logger::Debug
<<"Calculated diff for "<<domain
<<", we had "<<diff
->removals
.size()<<" removals and "<<diff
->additions
.size()<<" additions"<<endl
;
439 zoneInfo
->ixfrDiffs
.push_back(std::move(diff
));
442 // Clean up the diffs
443 while (zoneInfo
->ixfrDiffs
.size() > keep
) {
444 zoneInfo
->ixfrDiffs
.erase(zoneInfo
->ixfrDiffs
.begin());
447 g_log
<<Logger::Debug
<<"Zone "<<domain
<<" previously contained "<<(oldZoneInfo
? oldZoneInfo
->latestAXFR
.size() : 0)<<" entries, "<<records
.size()<<" now"<<endl
;
448 zoneInfo
->latestAXFR
= std::move(records
);
450 zoneInfo
->soaTTL
= soaTTL
;
451 updateCurrentZoneInfo(domain
, zoneInfo
);
452 } catch (PDNSException
&e
) {
453 g_stats
.incrementAXFRFailures(domain
);
454 g_log
<<Logger::Warning
<<"Could not save zone '"<<domain
<<"' to disk: "<<e
.reason
<<endl
;
455 } catch (runtime_error
&e
) {
456 g_stats
.incrementAXFRFailures(domain
);
457 g_log
<<Logger::Warning
<<"Could not save zone '"<<domain
<<"' to disk: "<<e
.what()<<endl
;
460 // Now clean up the directory
461 cleanUpDomain(domain
, keep
, workdir
);
462 } /* for (const auto &domain : domains) */
467 static bool checkQuery(const MOADNSParser
& mdp
, const ComboAddress
& saddr
, const bool udp
= true, const string
& logPrefix
="") {
468 vector
<string
> info_msg
;
470 g_log
<<Logger::Debug
<<logPrefix
<<"Had "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).getName()<<" query from "<<saddr
.toStringWithPort()<<endl
;
472 if (udp
&& mdp
.d_qtype
!= QType::SOA
&& mdp
.d_qtype
!= QType::IXFR
) {
473 info_msg
.push_back("QType is unsupported (" + QType(mdp
.d_qtype
).getName() + " is not in {SOA,IXFR})");
476 if (!udp
&& mdp
.d_qtype
!= QType::SOA
&& mdp
.d_qtype
!= QType::IXFR
&& mdp
.d_qtype
!= QType::AXFR
) {
477 info_msg
.push_back("QType is unsupported (" + QType(mdp
.d_qtype
).getName() + " is not in {SOA,IXFR,AXFR})");
481 if (g_domainConfigs
.find(mdp
.d_qname
) == g_domainConfigs
.end()) {
482 info_msg
.push_back("Domain name '" + mdp
.d_qname
.toLogString() + "' is not configured for distribution");
485 const auto zoneInfo
= getCurrentZoneInfo(mdp
.d_qname
);
486 if (zoneInfo
== nullptr) {
487 info_msg
.push_back("Domain has not been transferred yet");
492 if (!info_msg
.empty()) {
493 g_log
<<Logger::Warning
<<logPrefix
<<"Refusing "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).getName()<<" query from "<<saddr
.toStringWithPort();
494 g_log
<<Logger::Warning
<<": ";
496 for (const auto& s
: info_msg
) {
498 g_log
<<Logger::Warning
<<", ";
501 g_log
<<Logger::Warning
<<s
;
503 g_log
<<Logger::Warning
<<endl
;
511 * Returns a vector<uint8_t> that represents the full positive response to a SOA
512 * query. QNAME is read from mdp.
514 static bool makeSOAPacket(const MOADNSParser
& mdp
, vector
<uint8_t>& packet
) {
516 auto zoneInfo
= getCurrentZoneInfo(mdp
.d_qname
);
517 if (zoneInfo
== nullptr) {
521 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
522 pw
.getHeader()->id
= mdp
.d_header
.id
;
523 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
524 pw
.getHeader()->qr
= 1;
526 pw
.startRecord(mdp
.d_qname
, QType::SOA
, zoneInfo
->soaTTL
);
527 zoneInfo
->soa
->toPacket(pw
);
534 * Returns a vector<uint8_t> that represents the full REFUSED response to a
535 * query. QNAME and type are read from mdp.
537 static bool makeRefusedPacket(const MOADNSParser
& mdp
, vector
<uint8_t>& packet
) {
538 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
539 pw
.getHeader()->id
= mdp
.d_header
.id
;
540 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
541 pw
.getHeader()->qr
= 1;
542 pw
.getHeader()->rcode
= RCode::Refused
;
547 static vector
<uint8_t> getSOAPacket(const MOADNSParser
& mdp
, const shared_ptr
<SOARecordContent
>& soa
, uint32_t soaTTL
) {
548 vector
<uint8_t> packet
;
549 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
550 pw
.getHeader()->id
= mdp
.d_header
.id
;
551 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
552 pw
.getHeader()->qr
= 1;
555 pw
.startRecord(mdp
.d_qname
, QType::SOA
, soaTTL
);
561 static bool sendPacketOverTCP(int fd
, const std::vector
<uint8_t>& packet
)
564 sendBuf
[0]=packet
.size()/256;
565 sendBuf
[1]=packet
.size()%256;
567 writen2(fd
, sendBuf
, 2);
568 writen2(fd
, &packet
[0], packet
.size());
572 static bool addRecordToWriter(DNSPacketWriter
& pw
, const DNSName
& zoneName
, const DNSRecord
& record
, bool compress
)
574 pw
.startRecord(record
.d_name
+ zoneName
, record
.d_type
, record
.d_ttl
, QClass::IN
, DNSResourceRecord::ANSWER
, compress
);
575 record
.d_content
->toPacket(pw
);
576 if (pw
.size() > 65535) {
583 template <typename T
> static bool sendRecordsOverTCP(int fd
, const MOADNSParser
& mdp
, const T
& records
)
585 vector
<uint8_t> packet
;
587 for (auto it
= records
.cbegin(); it
!= records
.cend();) {
588 bool recordsAdded
= false;
590 DNSPacketWriter
pw(packet
, mdp
.d_qname
, mdp
.d_qtype
);
591 pw
.getHeader()->id
= mdp
.d_header
.id
;
592 pw
.getHeader()->rd
= mdp
.d_header
.rd
;
593 pw
.getHeader()->qr
= 1;
595 while (it
!= records
.cend()) {
596 if (it
->d_type
== QType::SOA
) {
601 if (addRecordToWriter(pw
, mdp
.d_qname
, *it
, g_compress
)) {
608 sendPacketOverTCP(fd
, packet
);
610 if (it
== records
.cbegin()) {
611 /* something is wrong */
619 if (it
== records
.cend() && recordsAdded
) {
621 sendPacketOverTCP(fd
, packet
);
629 static bool handleAXFR(int fd
, const MOADNSParser
& mdp
) {
630 /* we get a shared pointer of the zone info that we can't modify, ever.
631 A newer one may arise in the meantime, but this one will stay valid
635 g_stats
.incrementAXFRinQueries(mdp
.d_qname
);
637 auto zoneInfo
= getCurrentZoneInfo(mdp
.d_qname
);
638 if (zoneInfo
== nullptr) {
642 shared_ptr
<SOARecordContent
> soa
= zoneInfo
->soa
;
643 uint32_t soaTTL
= zoneInfo
->soaTTL
;
644 const records_t
& records
= zoneInfo
->latestAXFR
;
647 const auto soaPacket
= getSOAPacket(mdp
, soa
, soaTTL
);
648 if (!sendPacketOverTCP(fd
, soaPacket
)) {
652 if (!sendRecordsOverTCP(fd
, mdp
, records
)) {
657 if (!sendPacketOverTCP(fd
, soaPacket
)) {
664 /* Produces an IXFR if one can be made according to the rules in RFC 1995 and
665 * creates a SOA or AXFR packet when required by the RFC.
667 static bool handleIXFR(int fd
, const ComboAddress
& destination
, const MOADNSParser
& mdp
, const shared_ptr
<SOARecordContent
>& clientSOA
) {
668 vector
<std::shared_ptr
<ixfrdiff_t
>> toSend
;
670 /* we get a shared pointer of the zone info that we can't modify, ever.
671 A newer one may arise in the meantime, but this one will stay valid
675 g_stats
.incrementIXFRinQueries(mdp
.d_qname
);
677 auto zoneInfo
= getCurrentZoneInfo(mdp
.d_qname
);
678 if (zoneInfo
== nullptr) {
682 uint32_t ourLatestSerial
= zoneInfo
->soa
->d_st
.serial
;
684 if (rfc1982LessThan(ourLatestSerial
, clientSOA
->d_st
.serial
) || ourLatestSerial
== clientSOA
->d_st
.serial
) {
685 /* RFC 1995 Section 2
686 * If an IXFR query with the same or newer version number than that of
687 * the server is received, it is replied to with a single SOA record of
688 * the server's current version.
690 vector
<uint8_t> packet
;
691 bool ret
= makeSOAPacket(mdp
, packet
);
693 sendPacketOverTCP(fd
, packet
);
698 // as we use push_back in the updater, we know the vector is sorted as oldest first
699 bool shouldAdd
= false;
700 // Get all relevant IXFR differences
701 for (const auto& diff
: zoneInfo
->ixfrDiffs
) {
703 toSend
.push_back(diff
);
706 if (diff
->oldSOA
->d_st
.serial
== clientSOA
->d_st
.serial
) {
707 toSend
.push_back(diff
);
708 // Add all consecutive diffs
713 if (toSend
.empty()) {
714 // FIXME: incrementIXFRFallbacks
715 g_log
<<Logger::Warning
<<"No IXFR available from serial "<<clientSOA
->d_st
.serial
<<" for zone "<<mdp
.d_qname
<<", attempting to send AXFR"<<endl
;
716 return handleAXFR(fd
, mdp
);
719 std::vector
<std::vector
<uint8_t>> packets
;
720 for (const auto& diff
: toSend
) {
721 /* An IXFR packet's ANSWER section looks as follows:
724 * ... removed records ...
726 * ... added records ...
730 const auto newSOAPacket
= getSOAPacket(mdp
, diff
->newSOA
, diff
->newSOATTL
);
731 const auto oldSOAPacket
= getSOAPacket(mdp
, diff
->oldSOA
, diff
->oldSOATTL
);
733 if (!sendPacketOverTCP(fd
, newSOAPacket
)) {
737 if (!sendPacketOverTCP(fd
, oldSOAPacket
)) {
741 if (!sendRecordsOverTCP(fd
, mdp
, diff
->removals
)) {
745 if (!sendPacketOverTCP(fd
, newSOAPacket
)) {
749 if (!sendRecordsOverTCP(fd
, mdp
, diff
->additions
)) {
753 if (!sendPacketOverTCP(fd
, newSOAPacket
)) {
761 static bool allowedByACL(const ComboAddress
& addr
) {
762 return g_acl
.match(addr
);
765 static void handleUDPRequest(int fd
, boost::any
&) {
766 // TODO make the buffer-size configurable
769 socklen_t fromlen
= sizeof(saddr
);
770 int res
= recvfrom(fd
, buf
, sizeof(buf
), 0, (struct sockaddr
*) &saddr
, &fromlen
);
773 g_log
<<Logger::Warning
<<"Got an empty message from "<<saddr
.toStringWithPort()<<endl
;
778 auto savedErrno
= errno
;
779 g_log
<<Logger::Warning
<<"Could not read message from "<<saddr
.toStringWithPort()<<": "<<strerror(savedErrno
)<<endl
;
783 if (saddr
== ComboAddress("0.0.0.0", 0)) {
784 g_log
<<Logger::Warning
<<"Could not determine source of message"<<endl
;
788 if (!allowedByACL(saddr
)) {
789 g_log
<<Logger::Warning
<<"UDP query from "<<saddr
.toString()<<" is not allowed, dropping"<<endl
;
793 MOADNSParser
mdp(true, string(buf
, res
));
794 vector
<uint8_t> packet
;
795 if (checkQuery(mdp
, saddr
)) {
796 /* RFC 1995 Section 2
797 * Transport of a query may be by either UDP or TCP. If an IXFR query
798 * is via UDP, the IXFR server may attempt to reply using UDP if the
799 * entire response can be contained in a single DNS packet. If the UDP
800 * reply does not fit, the query is responded to with a single SOA
801 * record of the server's current version to inform the client that a
802 * TCP query should be initiated.
804 * Let's not complicate this with IXFR over UDP (and looking if we need to truncate etc).
805 * Just send the current SOA and let the client try over TCP
807 g_stats
.incrementSOAinQueries(mdp
.d_qname
); // FIXME: this also counts IXFR queries (but the response is the same as to a SOA query)
808 makeSOAPacket(mdp
, packet
);
810 makeRefusedPacket(mdp
, packet
);
813 if(sendto(fd
, &packet
[0], packet
.size(), 0, (struct sockaddr
*) &saddr
, fromlen
) < 0) {
814 auto savedErrno
= errno
;
815 g_log
<<Logger::Warning
<<"Could not send reply for "<<mdp
.d_qname
<<"|"<<QType(mdp
.d_qtype
).getName()<<" to "<<saddr
.toStringWithPort()<<": "<<strerror(savedErrno
)<<endl
;
820 static void handleTCPRequest(int fd
, boost::any
&) {
825 cfd
= SAccept(fd
, saddr
);
827 } catch(runtime_error
&e
) {
828 g_log
<<Logger::Error
<<e
.what()<<endl
;
832 if (saddr
== ComboAddress("0.0.0.0", 0)) {
833 g_log
<<Logger::Warning
<<"Could not determine source of message"<<endl
;
838 if (!allowedByACL(saddr
)) {
839 g_log
<<Logger::Warning
<<"TCP query from "<<saddr
.toString()<<" is not allowed, dropping"<<endl
;
845 std::lock_guard
<std::mutex
> lg(g_tcpRequestFDsMutex
);
846 g_tcpRequestFDs
.push({cfd
, saddr
});
848 g_tcpHandlerCV
.notify_one();
851 /* Thread to handle TCP traffic
853 static void tcpWorker(int tid
) {
854 setThreadName("ixfrdist/tcpWor");
855 string prefix
= "TCP Worker " + std::to_string(tid
) + ": ";
858 g_log
<<Logger::Debug
<<prefix
<<"ready for a new request!"<<endl
;
859 std::unique_lock
<std::mutex
> lk(g_tcpRequestFDsMutex
);
860 g_tcpHandlerCV
.wait(lk
, []{return g_tcpRequestFDs
.size() || g_exiting
;});
862 g_log
<<Logger::Debug
<<prefix
<<"Stopping thread"<<endl
;
865 g_log
<<Logger::Debug
<<prefix
<<"Going to handle a query"<<endl
;
866 auto request
= g_tcpRequestFDs
.front();
867 g_tcpRequestFDs
.pop();
870 int cfd
= request
.first
;
871 ComboAddress saddr
= request
.second
;
877 readn2(cfd
, &toRead
, sizeof(toRead
));
878 toRead
= std::min(ntohs(toRead
), static_cast<uint16_t>(sizeof(buf
)));
879 res
= readn2WithTimeout(cfd
, &buf
, toRead
, 2);
880 g_log
<<Logger::Debug
<<prefix
<<"Had message of "<<std::to_string(toRead
)<<" bytes from "<<saddr
.toStringWithPort()<<endl
;
881 } catch (runtime_error
&e
) {
882 g_log
<<Logger::Warning
<<prefix
<<"Could not read message from "<<saddr
.toStringWithPort()<<": "<<e
.what()<<endl
;
888 MOADNSParser
mdp(true, string(buf
, res
));
890 if (!checkQuery(mdp
, saddr
, false, prefix
)) {
895 if (mdp
.d_qtype
== QType::SOA
) {
896 vector
<uint8_t> packet
;
897 bool ret
= makeSOAPacket(mdp
, packet
);
902 sendPacketOverTCP(cfd
, packet
);
904 else if (mdp
.d_qtype
== QType::AXFR
) {
905 if (!handleAXFR(cfd
, mdp
)) {
910 else if (mdp
.d_qtype
== QType::IXFR
) {
911 /* RFC 1995 section 3:
912 * The IXFR query packet format is the same as that of a normal DNS
913 * query, but with the query type being IXFR and the authority section
914 * containing the SOA record of client's version of the zone.
916 shared_ptr
<SOARecordContent
> clientSOA
;
917 for (auto &answer
: mdp
.d_answers
) {
918 // from dnsparser.hh:
919 // typedef vector<pair<DNSRecord, uint16_t > > answers_t;
920 if (answer
.first
.d_type
== QType::SOA
&& answer
.first
.d_place
== DNSResourceRecord::AUTHORITY
) {
921 clientSOA
= getRR
<SOARecordContent
>(answer
.first
);
922 if (clientSOA
!= nullptr) {
926 } /* for (auto const &answer : mdp.d_answers) */
928 if (clientSOA
== nullptr) {
929 g_log
<<Logger::Warning
<<prefix
<<"IXFR request packet did not contain a SOA record in the AUTHORITY section"<<endl
;
934 if (!handleIXFR(cfd
, saddr
, mdp
, clientSOA
)) {
938 } /* if (mdp.d_qtype == QType::IXFR) */
941 } catch (const MOADNSException
&mde
) {
942 g_log
<<Logger::Warning
<<prefix
<<"Could not parse DNS packet from "<<saddr
.toStringWithPort()<<": "<<mde
.what()<<endl
;
943 } catch (runtime_error
&e
) {
944 g_log
<<Logger::Warning
<<prefix
<<"Could not write reply to "<<saddr
.toStringWithPort()<<": "<<e
.what()<<endl
;
955 /* Parses the configuration file in configpath into config, adding defaults for
956 * missing parameters (if applicable), returning true if the config file was
957 * good, false otherwise. Will log all issues with the config
959 static bool parseAndCheckConfig(const string
& configpath
, YAML::Node
& config
) {
960 g_log
<<Logger::Info
<<"Loading configuration file from "<<configpath
<<endl
;
962 config
= YAML::LoadFile(configpath
);
963 } catch (const runtime_error
&e
) {
964 g_log
<<Logger::Error
<<"Unable to load configuration file '"<<configpath
<<"': "<<e
.what()<<endl
;
970 if (config
["keep"]) {
972 config
["keep"].as
<uint16_t>();
973 } catch (const runtime_error
&e
) {
974 g_log
<<Logger::Error
<<"Unable to read 'keep' value: "<<e
.what()<<endl
;
981 if (config
["axfr-max-records"]) {
983 config
["axfr-max-records"].as
<uint32_t>();
984 } catch (const runtime_error
&e
) {
985 g_log
<<Logger::Error
<<"Unable to read 'axfr-max-records' value: "<<e
.what()<<endl
;
988 config
["axfr-max-records"] = 0;
991 if (config
["axfr-timeout"]) {
993 config
["axfr-timeout"].as
<uint16_t>();
994 } catch (const runtime_error
&e
) {
995 g_log
<<Logger::Error
<<"Unable to read 'axfr-timeout' value: "<<e
.what()<<endl
;
998 config
["axfr-timeout"] = 20;
1001 if (config
["failed-soa-retry"]) {
1003 config
["failed-soa-retry"].as
<uint16_t>();
1004 } catch (const runtime_error
&e
) {
1005 g_log
<<Logger::Error
<<"Unable to read 'failed-soa-retry' value: "<<e
.what()<<endl
;
1008 config
["failed-soa-retry"] = 30;
1011 if (config
["tcp-in-threads"]) {
1013 config
["tcp-in-threads"].as
<uint16_t>();
1014 } catch (const runtime_error
&e
) {
1015 g_log
<<Logger::Error
<<"Unable to read 'tcp-in-threads' value: "<<e
.what()<<endl
;
1018 config
["tcp-in-threads"] = 10;
1021 if (config
["listen"]) {
1023 config
["listen"].as
<vector
<ComboAddress
>>();
1024 } catch (const runtime_error
&e
) {
1025 g_log
<<Logger::Error
<<"Unable to read 'listen' value: "<<e
.what()<<endl
;
1029 config
["listen"].push_back("127.0.0.1:53");
1030 config
["listen"].push_back("[::1]:53");
1033 if (config
["acl"]) {
1035 config
["acl"].as
<vector
<string
>>();
1036 } catch (const runtime_error
&e
) {
1037 g_log
<<Logger::Error
<<"Unable to read 'acl' value: "<<e
.what()<<endl
;
1041 config
["acl"].push_back("127.0.0.0/8");
1042 config
["acl"].push_back("::1/128");
1045 if (config
["work-dir"]) {
1047 config
["work-dir"].as
<string
>();
1048 } catch(const runtime_error
&e
) {
1049 g_log
<<Logger::Error
<<"Unable to read 'work-dir' value: "<<e
.what()<<endl
;
1054 config
["work-dir"] = getcwd(tmp
, sizeof(tmp
)) ? string(tmp
) : "";;
1057 if (config
["uid"]) {
1059 config
["uid"].as
<string
>();
1060 } catch(const runtime_error
&e
) {
1061 g_log
<<Logger::Error
<<"Unable to read 'uid' value: "<<e
.what()<<endl
;
1066 if (config
["gid"]) {
1068 config
["gid"].as
<string
>();
1069 } catch(const runtime_error
&e
) {
1070 g_log
<<Logger::Error
<<"Unable to read 'gid' value: "<<e
.what()<<endl
;
1075 if (config
["domains"]) {
1076 if (config
["domains"].size() == 0) {
1077 g_log
<<Logger::Error
<<"No domains configured"<<endl
;
1080 for (auto const &domain
: config
["domains"]) {
1082 if (!domain
["domain"]) {
1083 g_log
<<Logger::Error
<<"An entry in 'domains' is missing a 'domain' key!"<<endl
;
1087 domain
["domain"].as
<DNSName
>();
1088 } catch (const runtime_error
&e
) {
1089 g_log
<<Logger::Error
<<"Unable to read domain '"<<domain
["domain"].as
<string
>()<<"': "<<e
.what()<<endl
;
1092 if (!domain
["master"]) {
1093 g_log
<<Logger::Error
<<"Domain '"<<domain
["domain"].as
<string
>()<<"' has no master configured!"<<endl
;
1097 domain
["master"].as
<ComboAddress
>();
1098 } catch (const runtime_error
&e
) {
1099 g_log
<<Logger::Error
<<"Unable to read domain '"<<domain
["domain"].as
<string
>()<<"' master address: "<<e
.what()<<endl
;
1104 g_log
<<Logger::Error
<<"No domains configured"<<endl
;
1108 if (config
["compress"]) {
1110 config
["compress"].as
<bool>();
1112 catch (const runtime_error
&e
) {
1113 g_log
<<Logger::Error
<<"Unable to read 'compress' value: "<<e
.what()<<endl
;
1118 config
["compress"] = false;
1121 if (config
["webserver-address"]) {
1123 config
["webserver-address"].as
<ComboAddress
>();
1125 catch (const runtime_error
&e
) {
1126 g_log
<<Logger::Error
<<"Unable to read 'webserver-address' value: "<<e
.what()<<endl
;
1131 if (config
["webserver-acl"]) {
1133 config
["webserver-acl"].as
<vector
<Netmask
>>();
1135 catch (const runtime_error
&e
) {
1136 g_log
<<Logger::Error
<<"Unable to read 'webserver-acl' value: "<<e
.what()<<endl
;
1141 if (config
["webserver-loglevel"]) {
1143 config
["webserver-loglevel"].as
<string
>();
1145 catch (const runtime_error
&e
) {
1146 g_log
<<Logger::Error
<<"Unable to read 'webserver-loglevel' value: "<<e
.what()<<endl
;
1154 int main(int argc
, char** argv
) {
1155 g_log
.setLoglevel(Logger::Notice
);
1156 g_log
.toConsole(Logger::Notice
);
1157 g_log
.setPrefixed(true);
1158 g_log
.disableSyslog(true);
1159 g_log
.setTimestamps(false);
1160 po::variables_map g_vm
;
1162 po::options_description
desc("IXFR distribution tool");
1164 ("help", "produce help message")
1165 ("version", "Display the version of ixfrdist")
1166 ("verbose", "Be verbose")
1167 ("debug", "Be even more verbose")
1168 ("config", po::value
<string
>()->default_value(SYSCONFDIR
+ string("/ixfrdist.yml")), "Configuration file to use")
1171 po::store(po::command_line_parser(argc
, argv
).options(desc
).run(), g_vm
);
1174 if (g_vm
.count("help") > 0) {
1176 return EXIT_SUCCESS
;
1179 if (g_vm
.count("version") > 0) {
1180 cout
<<"ixfrdist "<<VERSION
<<endl
;
1181 return EXIT_SUCCESS
;
1183 } catch (po::error
&e
) {
1184 g_log
<<Logger::Error
<<e
.what()<<". See `ixfrdist --help` for valid options"<<endl
;
1185 return(EXIT_FAILURE
);
1188 bool had_error
= false;
1190 if (g_vm
.count("verbose")) {
1191 g_log
.setLoglevel(Logger::Info
);
1192 g_log
.toConsole(Logger::Info
);
1195 if (g_vm
.count("debug") > 0) {
1196 g_log
.setLoglevel(Logger::Debug
);
1197 g_log
.toConsole(Logger::Debug
);
1200 g_log
<<Logger::Notice
<<"IXFR distributor version "<<VERSION
<<" starting up!"<<endl
;
1203 if (!parseAndCheckConfig(g_vm
["config"].as
<string
>(), config
)) {
1204 // parseAndCheckConfig already logged whatever was wrong
1205 return EXIT_FAILURE
;
1208 /* From hereon out, we known that all the values in config are valid. */
1210 for (auto const &domain
: config
["domains"]) {
1211 set
<ComboAddress
> s
;
1212 s
.insert(domain
["master"].as
<ComboAddress
>());
1213 g_domainConfigs
[domain
["domain"].as
<DNSName
>()].masters
= s
;
1214 g_stats
.registerDomain(domain
["domain"].as
<DNSName
>());
1217 for (const auto &addr
: config
["acl"].as
<vector
<string
>>()) {
1219 g_acl
.addMask(addr
);
1220 } catch (const NetmaskException
&e
) {
1221 g_log
<<Logger::Error
<<e
.reason
<<endl
;
1225 g_log
<<Logger::Notice
<<"ACL set to "<<g_acl
.toString()<<"."<<endl
;
1227 if (config
["compress"]) {
1228 g_compress
= config
["compress"].as
<bool>();
1230 g_log
<<Logger::Notice
<<"Record compression is enabled."<<endl
;
1234 FDMultiplexer
* fdm
= FDMultiplexer::getMultiplexerSilent();
1235 if (fdm
== nullptr) {
1236 g_log
<<Logger::Error
<<"Could not enable a multiplexer for the listen sockets!"<<endl
;
1237 return EXIT_FAILURE
;
1240 set
<int> allSockets
;
1241 for (const auto& addr
: config
["listen"].as
<vector
<ComboAddress
>>()) {
1242 for (const auto& stype
: {SOCK_DGRAM
, SOCK_STREAM
}) {
1244 int s
= SSocket(addr
.sin4
.sin_family
, stype
, 0);
1248 if (stype
== SOCK_STREAM
) {
1249 SListen(s
, 30); // TODO make this configurable
1251 fdm
->addReadFD(s
, stype
== SOCK_DGRAM
? handleUDPRequest
: handleTCPRequest
);
1252 allSockets
.insert(s
);
1253 } catch(runtime_error
&e
) {
1254 g_log
<<Logger::Error
<<e
.what()<<endl
;
1263 if (config
["gid"]) {
1264 string gid
= config
["gid"].as
<string
>();
1265 if (!(newgid
= atoi(gid
.c_str()))) {
1266 struct group
*gr
= getgrnam(gid
.c_str());
1267 if (gr
== nullptr) {
1268 g_log
<<Logger::Error
<<"Can not determine group-id for gid "<<gid
<<endl
;
1271 newgid
= gr
->gr_gid
;
1274 g_log
<<Logger::Notice
<<"Dropping effective group-id to "<<newgid
<<endl
;
1275 if (setgid(newgid
) < 0) {
1276 g_log
<<Logger::Error
<<"Could not set group id to "<<newgid
<<": "<<stringerror()<<endl
;
1281 if (config
["webserver-address"]) {
1283 wsACL
.addMask("127.0.0.0/8");
1284 wsACL
.addMask("::1/128");
1286 if (config
["webserver-acl"]) {
1288 for (const auto &acl
: config
["webserver-acl"].as
<vector
<Netmask
>>()) {
1293 string loglevel
= "normal";
1294 if (config
["webserver-loglevel"]) {
1295 loglevel
= config
["webserver-loglevel"].as
<string
>();
1298 // Launch the webserver!
1300 std::thread(&IXFRDistWebServer::go
, IXFRDistWebServer(config
["webserver-address"].as
<ComboAddress
>(), wsACL
, loglevel
)).detach();
1301 } catch (const PDNSException
&e
) {
1302 g_log
<<Logger::Error
<<"Unable to start webserver: "<<e
.reason
<<endl
;
1309 if (config
["uid"]) {
1310 string uid
= config
["uid"].as
<string
>();
1311 if (!(newuid
= atoi(uid
.c_str()))) {
1312 struct passwd
*pw
= getpwnam(uid
.c_str());
1313 if (pw
== nullptr) {
1314 g_log
<<Logger::Error
<<"Can not determine user-id for uid "<<uid
<<endl
;
1317 newuid
= pw
->pw_uid
;
1321 struct passwd
*pw
= getpwuid(newuid
);
1322 if (pw
== nullptr) {
1323 if (setgroups(0, nullptr) < 0) {
1324 g_log
<<Logger::Error
<<"Unable to drop supplementary gids: "<<stringerror()<<endl
;
1328 if (initgroups(pw
->pw_name
, newgid
) < 0) {
1329 g_log
<<Logger::Error
<<"Unable to set supplementary groups: "<<stringerror()<<endl
;
1334 g_log
<<Logger::Notice
<<"Dropping effective user-id to "<<newuid
<<endl
;
1335 if (setuid(newuid
) < 0) {
1336 g_log
<<Logger::Error
<<"Could not set user id to "<<newuid
<<": "<<stringerror()<<endl
;
1342 // We have already sent the errors to stderr, just die
1343 return EXIT_FAILURE
;
1346 // It all starts here
1347 signal(SIGTERM
, handleSignal
);
1348 signal(SIGINT
, handleSignal
);
1349 signal(SIGPIPE
, SIG_IGN
);
1351 // Init the things we need
1356 std::thread
ut(updateThread
,
1357 config
["work-dir"].as
<string
>(),
1358 config
["keep"].as
<uint16_t>(),
1359 config
["axfr-timeout"].as
<uint16_t>(),
1360 config
["failed-soa-retry"].as
<uint16_t>(),
1361 config
["axfr-max-records"].as
<uint32_t>());
1363 vector
<std::thread
> tcpHandlers
;
1364 tcpHandlers
.reserve(config
["tcp-in-threads"].as
<uint16_t>());
1365 for (size_t i
= 0; i
< tcpHandlers
.capacity(); ++i
) {
1366 tcpHandlers
.push_back(std::thread(tcpWorker
, i
));
1371 gettimeofday(&now
, 0);
1374 g_log
<<Logger::Debug
<<"Closing listening sockets"<<endl
;
1375 for (const int& fd
: allSockets
) {
1378 } catch(PDNSException
&e
) {
1379 g_log
<<Logger::Error
<<e
.reason
<<endl
;
1385 g_log
<<Logger::Debug
<<"Waiting for all threads to stop"<<endl
;
1386 g_tcpHandlerCV
.notify_all();
1388 for (auto &t
: tcpHandlers
) {
1391 g_log
<<Logger::Notice
<<"IXFR distributor stopped"<<endl
;
1392 return EXIT_SUCCESS
;