]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/ixfrdist.cc
Merge pull request #6979 from rgacogne/rec-cname-authority
[thirdparty/pdns.git] / pdns / ixfrdist.cc
1 /*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 #include <boost/program_options.hpp>
26 #include <arpa/inet.h>
27 #include <sys/types.h>
28 #include <grp.h>
29 #include <pwd.h>
30 #include <sys/stat.h>
31 #include <mutex>
32 #include <thread>
33 #include "threadname.hh"
34 #include <dirent.h>
35 #include <queue>
36 #include <condition_variable>
37 #include "ixfr.hh"
38 #include "ixfrutils.hh"
39 #include "resolver.hh"
40 #include "dns_random.hh"
41 #include "sstuff.hh"
42 #include "mplexer.hh"
43 #include "misc.hh"
44 #include "iputils.hh"
45 #include "logger.hh"
46 #include <yaml-cpp/yaml.h>
47
48 /* BEGIN Needed because of deeper dependencies */
49 #include "arguments.hh"
50 #include "statbag.hh"
51 StatBag S;
52
53 ArgvMap &arg()
54 {
55 static ArgvMap theArg;
56 return theArg;
57 }
58 /* END Needed because of deeper dependencies */
59
60 // Allows reading/writing ComboAddresses and DNSNames in YAML-cpp
61 namespace YAML {
62 template<>
63 struct convert<ComboAddress> {
64 static Node encode(const ComboAddress& rhs) {
65 return Node(rhs.toStringWithPort());
66 }
67 static bool decode(const Node& node, ComboAddress& rhs) {
68 if (!node.IsScalar()) {
69 return false;
70 }
71 try {
72 rhs = ComboAddress(node.as<string>(), 53);
73 return true;
74 } catch(const runtime_error &e) {
75 return false;
76 } catch (const PDNSException &e) {
77 return false;
78 }
79 }
80 };
81
82 template<>
83 struct convert<DNSName> {
84 static Node encode(const DNSName& rhs) {
85 return Node(rhs.toStringRootDot());
86 }
87 static bool decode(const Node& node, DNSName& rhs) {
88 if (!node.IsScalar()) {
89 return false;
90 }
91 try {
92 rhs = DNSName(node.as<string>());
93 return true;
94 } catch(const runtime_error &e) {
95 return false;
96 } catch (const PDNSException &e) {
97 return false;
98 }
99 }
100 };
101 } // namespace YAML
102
103 struct ixfrdiff_t {
104 shared_ptr<SOARecordContent> oldSOA;
105 shared_ptr<SOARecordContent> newSOA;
106 vector<DNSRecord> removals;
107 vector<DNSRecord> additions;
108 };
109
110 struct ixfrinfo_t {
111 shared_ptr<SOARecordContent> soa; // The SOA of the latestAXFR
112 records_t latestAXFR; // The most recent AXFR
113 vector<std::shared_ptr<ixfrdiff_t>> ixfrDiffs;
114 };
115
116 // Why a struct? This way we can add more options to a domain in the future
117 struct ixfrdistdomain_t {
118 set<ComboAddress> masters; // A set so we can do multiple master addresses in the future
119 };
120
121 // This contains the configuration for each domain
122 static map<DNSName, ixfrdistdomain_t> g_domainConfigs;
123
124 // Map domains and their data
125 static std::map<DNSName, std::shared_ptr<ixfrinfo_t>> g_soas;
126 static std::mutex g_soas_mutex;
127
128 // Condition variable for TCP handling
129 static std::condition_variable g_tcpHandlerCV;
130 static std::queue<pair<int, ComboAddress>> g_tcpRequestFDs;
131 static std::mutex g_tcpRequestFDsMutex;
132
133 namespace po = boost::program_options;
134
135 static bool g_exiting = false;
136
137 static NetmaskGroup g_acl;
138 static bool g_compress = false;
139
140 static void handleSignal(int signum) {
141 g_log<<Logger::Notice<<"Got "<<strsignal(signum)<<" signal";
142 if (g_exiting) {
143 g_log<<Logger::Notice<<", this is the second time we were asked to stop, forcefully exiting"<<endl;
144 exit(EXIT_FAILURE);
145 }
146 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;
147 g_exiting = true;
148 }
149
150 static void usage(po::options_description &desc) {
151 cerr << "Usage: ixfrdist [OPTION]..."<<endl;
152 cerr << desc << "\n";
153 }
154
155 // The compiler does not like using rfc1982LessThan in std::sort directly
156 static bool sortSOA(uint32_t i, uint32_t j) {
157 return rfc1982LessThan(i, j);
158 }
159
160 static void cleanUpDomain(const DNSName& domain, const uint16_t& keep, const string& workdir) {
161 string dir = workdir + "/" + domain.toString();
162 DIR *dp;
163 dp = opendir(dir.c_str());
164 if (dp == nullptr) {
165 return;
166 }
167 vector<uint32_t> zoneVersions;
168 struct dirent *d;
169 while ((d = readdir(dp)) != nullptr) {
170 if(!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) {
171 continue;
172 }
173 zoneVersions.push_back(std::stoi(d->d_name));
174 }
175 closedir(dp);
176 g_log<<Logger::Info<<"Found "<<zoneVersions.size()<<" versions of "<<domain<<", asked to keep "<<keep<<", ";
177 if (zoneVersions.size() <= keep) {
178 g_log<<Logger::Info<<"not cleaning up"<<endl;
179 return;
180 }
181 g_log<<Logger::Info<<"cleaning up the oldest "<<zoneVersions.size() - keep<<endl;
182
183 // Sort the versions
184 std::sort(zoneVersions.begin(), zoneVersions.end(), sortSOA);
185
186 // And delete all the old ones
187 {
188 // Lock to ensure no one reads this.
189 std::lock_guard<std::mutex> guard(g_soas_mutex);
190 for (auto iter = zoneVersions.cbegin(); iter != zoneVersions.cend() - keep; ++iter) {
191 string fname = dir + "/" + std::to_string(*iter);
192 g_log<<Logger::Debug<<"Removing "<<fname<<endl;
193 unlink(fname.c_str());
194 }
195 }
196 }
197
198 static shared_ptr<SOARecordContent> getSOAFromRecords(const records_t& records) {
199 for (const auto& dnsrecord : records) {
200 if (dnsrecord.d_type == QType::SOA) {
201 auto soa = getRR<SOARecordContent>(dnsrecord);
202 if (soa == nullptr) {
203 throw PDNSException("Unable to determine SOARecordContent from old records");
204 }
205 return soa;
206 }
207 }
208 throw PDNSException("No SOA in supplied records");
209 }
210
211 static void makeIXFRDiff(const records_t& from, const records_t& to, std::shared_ptr<ixfrdiff_t>& diff, const shared_ptr<SOARecordContent>& fromSOA = nullptr, const shared_ptr<SOARecordContent>& toSOA = nullptr) {
212 set_difference(from.cbegin(), from.cend(), to.cbegin(), to.cend(), back_inserter(diff->removals), from.value_comp());
213 set_difference(to.cbegin(), to.cend(), from.cbegin(), from.cend(), back_inserter(diff->additions), from.value_comp());
214 diff->oldSOA = fromSOA;
215 if (fromSOA == nullptr) {
216 diff->oldSOA = getSOAFromRecords(from);
217 }
218 diff->newSOA = toSOA;
219 if (toSOA == nullptr) {
220 diff->newSOA = getSOAFromRecords(to);
221 }
222 }
223
224 /* you can _never_ alter the content of the resulting shared pointer */
225 static std::shared_ptr<ixfrinfo_t> getCurrentZoneInfo(const DNSName& domain)
226 {
227 std::lock_guard<std::mutex> guard(g_soas_mutex);
228 return g_soas[domain];
229 }
230
231 static void updateCurrentZoneInfo(const DNSName& domain, std::shared_ptr<ixfrinfo_t>& newInfo)
232 {
233 std::lock_guard<std::mutex> guard(g_soas_mutex);
234 g_soas[domain] = newInfo;
235 }
236
237 void updateThread(const string& workdir, const uint16_t& keep, const uint16_t& axfrTimeout, const uint16_t& soaRetry) {
238 setThreadName("ixfrdist/update");
239 std::map<DNSName, time_t> lastCheck;
240
241 // Initialize the serials we have
242 for (const auto &domainConfig : g_domainConfigs) {
243 DNSName domain = domainConfig.first;
244 lastCheck[domain] = 0;
245 string dir = workdir + "/" + domain.toString();
246 try {
247 g_log<<Logger::Info<<"Trying to initially load domain "<<domain<<" from disk"<<endl;
248 auto serial = getSerialsFromDir(dir);
249 shared_ptr<SOARecordContent> soa;
250 {
251 string fname = workdir + "/" + domain.toString() + "/" + std::to_string(serial);
252 loadSOAFromDisk(domain, fname, soa);
253 records_t records;
254 if (soa != nullptr) {
255 loadZoneFromDisk(records, fname, domain);
256 }
257 auto zoneInfo = std::make_shared<ixfrinfo_t>();
258 zoneInfo->latestAXFR = std::move(records);
259 zoneInfo->soa = soa;
260 updateCurrentZoneInfo(domain, zoneInfo);
261 }
262 if (soa != nullptr) {
263 g_log<<Logger::Notice<<"Loaded zone "<<domain<<" with serial "<<soa->d_st.serial<<endl;
264 // Initial cleanup
265 cleanUpDomain(domain, keep, workdir);
266 }
267 } catch (runtime_error &e) {
268 // Most likely, the directory does not exist.
269 g_log<<Logger::Info<<e.what()<<", attempting to create"<<endl;
270 // Attempt to create it, if _that_ fails, there is no hope
271 if (mkdir(dir.c_str(), 0777) == -1 && errno != EEXIST) {
272 g_log<<Logger::Error<<"Could not create '"<<dir<<"': "<<strerror(errno)<<endl;
273 exit(EXIT_FAILURE);
274 }
275 }
276 }
277
278 g_log<<Logger::Notice<<"Update Thread started"<<endl;
279
280 while (true) {
281 if (g_exiting) {
282 g_log<<Logger::Notice<<"UpdateThread stopped"<<endl;
283 break;
284 }
285 time_t now = time(nullptr);
286 for (const auto &domainConfig : g_domainConfigs) {
287
288 if (g_exiting) {
289 break;
290 }
291
292 DNSName domain = domainConfig.first;
293 shared_ptr<SOARecordContent> current_soa;
294 const auto& zoneInfo = getCurrentZoneInfo(domain);
295 if (zoneInfo != nullptr) {
296 current_soa = zoneInfo->soa;
297 }
298
299 auto& zoneLastCheck = lastCheck[domain];
300 if ((current_soa != nullptr && now - zoneLastCheck < current_soa->d_st.refresh) || // Only check if we have waited `refresh` seconds
301 (current_soa == nullptr && now - zoneLastCheck < soaRetry)) { // Or if we could not get an update at all still, every 30 seconds
302 continue;
303 }
304
305 // TODO Keep track of 'down' masters
306 set<ComboAddress>::const_iterator it(domainConfig.second.masters.begin());
307 std::advance(it, random() % domainConfig.second.masters.size());
308 ComboAddress master = *it;
309
310 string dir = workdir + "/" + domain.toString();
311 g_log<<Logger::Info<<"Attempting to retrieve SOA Serial update for '"<<domain<<"' from '"<<master.toStringWithPort()<<"'"<<endl;
312 shared_ptr<SOARecordContent> sr;
313 try {
314 zoneLastCheck = now;
315 auto newSerial = getSerialFromMaster(master, domain, sr); // TODO TSIG
316 if(current_soa != nullptr) {
317 g_log<<Logger::Info<<"Got SOA Serial for "<<domain<<" from "<<master.toStringWithPort()<<": "<< newSerial<<", had Serial: "<<current_soa->d_st.serial;
318 if (newSerial == current_soa->d_st.serial) {
319 g_log<<Logger::Info<<", not updating."<<endl;
320 continue;
321 }
322 g_log<<Logger::Info<<", will update."<<endl;
323 }
324 } catch (runtime_error &e) {
325 g_log<<Logger::Warning<<"Unable to get SOA serial update for '"<<domain<<"' from master "<<master.toStringWithPort()<<": "<<e.what()<<endl;
326 continue;
327 }
328 // Now get the full zone!
329 g_log<<Logger::Info<<"Attempting to receive full zonedata for '"<<domain<<"'"<<endl;
330 ComboAddress local = master.isIPv4() ? ComboAddress("0.0.0.0") : ComboAddress("::");
331 TSIGTriplet tt;
332
333 // The *new* SOA
334 shared_ptr<SOARecordContent> soa;
335 records_t records;
336 try {
337 AXFRRetriever axfr(master, domain, tt, &local);
338 unsigned int nrecords=0;
339 Resolver::res_t nop;
340 vector<DNSRecord> chunk;
341 time_t t_start = time(nullptr);
342 time_t axfr_now = time(nullptr);
343 while(axfr.getChunk(nop, &chunk, (axfr_now - t_start + axfrTimeout))) {
344 for(auto& dr : chunk) {
345 if(dr.d_type == QType::TSIG)
346 continue;
347 if(!dr.d_name.isPartOf(domain)) {
348 throw PDNSException("Out-of-zone data received during AXFR of "+domain.toLogString());
349 }
350 dr.d_name.makeUsRelative(domain);
351 records.insert(dr);
352 nrecords++;
353 if (dr.d_type == QType::SOA) {
354 soa = getRR<SOARecordContent>(dr);
355 }
356 }
357 axfr_now = time(nullptr);
358 if (axfr_now - t_start > axfrTimeout) {
359 throw PDNSException("Total AXFR time exceeded!");
360 }
361 }
362 if (soa == nullptr) {
363 g_log<<Logger::Warning<<"No SOA was found in the AXFR of "<<domain<<endl;
364 continue;
365 }
366 g_log<<Logger::Notice<<"Retrieved all zone data for "<<domain<<". Received "<<nrecords<<" records."<<endl;
367 } catch (PDNSException &e) {
368 g_log<<Logger::Warning<<"Could not retrieve AXFR for '"<<domain<<"': "<<e.reason<<endl;
369 continue;
370 } catch (runtime_error &e) {
371 g_log<<Logger::Warning<<"Could not retrieve AXFR for zone '"<<domain<<"': "<<e.what()<<endl;
372 continue;
373 }
374
375 try {
376
377 writeZoneToDisk(records, domain, dir);
378 g_log<<Logger::Notice<<"Wrote zonedata for "<<domain<<" with serial "<<soa->d_st.serial<<" to "<<dir<<endl;
379
380 const auto oldZoneInfo = getCurrentZoneInfo(domain);
381 auto zoneInfo = std::make_shared<ixfrinfo_t>();
382
383 if (oldZoneInfo && !oldZoneInfo->latestAXFR.empty()) {
384 auto diff = std::make_shared<ixfrdiff_t>();
385 zoneInfo->ixfrDiffs = oldZoneInfo->ixfrDiffs;
386 g_log<<Logger::Debug<<"Calculating diff for "<<domain<<endl;
387 makeIXFRDiff(oldZoneInfo->latestAXFR, records, diff, oldZoneInfo->soa, soa);
388 g_log<<Logger::Debug<<"Calculated diff for "<<domain<<", we had "<<diff->removals.size()<<" removals and "<<diff->additions.size()<<" additions"<<endl;
389 zoneInfo->ixfrDiffs.push_back(std::move(diff));
390 }
391
392 // Clean up the diffs
393 while (zoneInfo->ixfrDiffs.size() > keep) {
394 zoneInfo->ixfrDiffs.erase(zoneInfo->ixfrDiffs.begin());
395 }
396
397 g_log<<Logger::Debug<<"Zone "<<domain<<" previously contained "<<(oldZoneInfo ? oldZoneInfo->latestAXFR.size() : 0)<<" entries, "<<records.size()<<" now"<<endl;
398 zoneInfo->latestAXFR = std::move(records);
399 zoneInfo->soa = soa;
400 updateCurrentZoneInfo(domain, zoneInfo);
401 } catch (PDNSException &e) {
402 g_log<<Logger::Warning<<"Could not save zone '"<<domain<<"' to disk: "<<e.reason<<endl;
403 } catch (runtime_error &e) {
404 g_log<<Logger::Warning<<"Could not save zone '"<<domain<<"' to disk: "<<e.what()<<endl;
405 }
406
407 // Now clean up the directory
408 cleanUpDomain(domain, keep, workdir);
409 } /* for (const auto &domain : domains) */
410 sleep(1);
411 } /* while (true) */
412 } /* updateThread */
413
414 static bool checkQuery(const MOADNSParser& mdp, const ComboAddress& saddr, const bool udp = true, const string& logPrefix="") {
415 vector<string> info_msg;
416
417 g_log<<Logger::Debug<<logPrefix<<"Had "<<mdp.d_qname<<"|"<<QType(mdp.d_qtype).getName()<<" query from "<<saddr.toStringWithPort()<<endl;
418
419 if (udp && mdp.d_qtype != QType::SOA && mdp.d_qtype != QType::IXFR) {
420 info_msg.push_back("QType is unsupported (" + QType(mdp.d_qtype).getName() + " is not in {SOA,IXFR})");
421 }
422
423 if (!udp && mdp.d_qtype != QType::SOA && mdp.d_qtype != QType::IXFR && mdp.d_qtype != QType::AXFR) {
424 info_msg.push_back("QType is unsupported (" + QType(mdp.d_qtype).getName() + " is not in {SOA,IXFR,AXFR})");
425 }
426
427 {
428 if (g_domainConfigs.find(mdp.d_qname) == g_domainConfigs.end()) {
429 info_msg.push_back("Domain name '" + mdp.d_qname.toLogString() + "' is not configured for distribution");
430 }
431 else {
432 const auto zoneInfo = getCurrentZoneInfo(mdp.d_qname);
433 if (zoneInfo == nullptr) {
434 info_msg.push_back("Domain has not been transferred yet");
435 }
436 }
437 }
438
439 if (!info_msg.empty()) {
440 g_log<<Logger::Warning<<logPrefix<<"Refusing "<<mdp.d_qname<<"|"<<QType(mdp.d_qtype).getName()<<" query from "<<saddr.toStringWithPort();
441 g_log<<Logger::Warning<<": ";
442 bool first = true;
443 for (const auto& s : info_msg) {
444 if (!first) {
445 g_log<<Logger::Warning<<", ";
446 }
447 first = false;
448 g_log<<Logger::Warning<<s;
449 }
450 g_log<<Logger::Warning<<endl;
451 return false;
452 }
453
454 return true;
455 }
456
457 /*
458 * Returns a vector<uint8_t> that represents the full positive response to a SOA
459 * query. QNAME is read from mdp.
460 */
461 static bool makeSOAPacket(const MOADNSParser& mdp, vector<uint8_t>& packet) {
462
463 auto zoneInfo = getCurrentZoneInfo(mdp.d_qname);
464 if (zoneInfo == nullptr) {
465 return false;
466 }
467
468 DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype);
469 pw.getHeader()->id = mdp.d_header.id;
470 pw.getHeader()->rd = mdp.d_header.rd;
471 pw.getHeader()->qr = 1;
472
473 pw.startRecord(mdp.d_qname, QType::SOA);
474 zoneInfo->soa->toPacket(pw);
475 pw.commit();
476
477 return true;
478 }
479
480 /*
481 * Returns a vector<uint8_t> that represents the full REFUSED response to a
482 * query. QNAME and type are read from mdp.
483 */
484 static bool makeRefusedPacket(const MOADNSParser& mdp, vector<uint8_t>& packet) {
485 DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype);
486 pw.getHeader()->id = mdp.d_header.id;
487 pw.getHeader()->rd = mdp.d_header.rd;
488 pw.getHeader()->qr = 1;
489 pw.getHeader()->rcode = RCode::Refused;
490
491 return true;
492 }
493
494 static vector<uint8_t> getSOAPacket(const MOADNSParser& mdp, const shared_ptr<SOARecordContent>& soa) {
495 vector<uint8_t> packet;
496 DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype);
497 pw.getHeader()->id = mdp.d_header.id;
498 pw.getHeader()->rd = mdp.d_header.rd;
499 pw.getHeader()->qr = 1;
500
501 // Add the first SOA
502 pw.startRecord(mdp.d_qname, QType::SOA);
503 soa->toPacket(pw);
504 pw.commit();
505 return packet;
506 }
507
508 static bool sendPacketOverTCP(int fd, const std::vector<uint8_t>& packet)
509 {
510 char sendBuf[2];
511 sendBuf[0]=packet.size()/256;
512 sendBuf[1]=packet.size()%256;
513
514 ssize_t send = writen2(fd, sendBuf, 2);
515 send += writen2(fd, &packet[0], packet.size());
516 return true;
517 }
518
519 static bool addRecordToWriter(DNSPacketWriter& pw, const DNSName& zoneName, const DNSRecord& record, bool compress)
520 {
521 pw.startRecord(record.d_name + zoneName, record.d_type, record.d_ttl, QClass::IN, DNSResourceRecord::ANSWER, compress);
522 record.d_content->toPacket(pw);
523 if (pw.size() > 65535) {
524 pw.rollback();
525 return false;
526 }
527 return true;
528 }
529
530 template <typename T> static bool sendRecordsOverTCP(int fd, const MOADNSParser& mdp, const T& records)
531 {
532 vector<uint8_t> packet;
533
534 for (auto it = records.cbegin(); it != records.cend();) {
535 bool recordsAdded = false;
536 packet.clear();
537 DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype);
538 pw.getHeader()->id = mdp.d_header.id;
539 pw.getHeader()->rd = mdp.d_header.rd;
540 pw.getHeader()->qr = 1;
541
542 while (it != records.cend()) {
543 if (it->d_type == QType::SOA) {
544 it++;
545 continue;
546 }
547
548 if (addRecordToWriter(pw, mdp.d_qname, *it, g_compress)) {
549 recordsAdded = true;
550 it++;
551 }
552 else {
553 if (recordsAdded) {
554 pw.commit();
555 sendPacketOverTCP(fd, packet);
556 }
557 if (it == records.cbegin()) {
558 /* something is wrong */
559 return false;
560 }
561
562 break;
563 }
564 }
565
566 if (it == records.cend() && recordsAdded) {
567 pw.commit();
568 sendPacketOverTCP(fd, packet);
569 }
570 }
571
572 return true;
573 }
574
575
576 static bool handleAXFR(int fd, const MOADNSParser& mdp) {
577 /* we get a shared pointer of the zone info that we can't modify, ever.
578 A newer one may arise in the meantime, but this one will stay valid
579 until we release it.
580 */
581 auto zoneInfo = getCurrentZoneInfo(mdp.d_qname);
582 if (zoneInfo == nullptr) {
583 return false;
584 }
585
586 shared_ptr<SOARecordContent> soa = zoneInfo->soa;
587 const records_t& records = zoneInfo->latestAXFR;
588
589 // Initial SOA
590 const auto soaPacket = getSOAPacket(mdp, soa);
591 if (!sendPacketOverTCP(fd, soaPacket)) {
592 return false;
593 }
594
595 if (!sendRecordsOverTCP(fd, mdp, records)) {
596 return false;
597 }
598
599 // Final SOA
600 if (!sendPacketOverTCP(fd, soaPacket)) {
601 return false;
602 }
603
604 return true;
605 }
606
607 /* Produces an IXFR if one can be made according to the rules in RFC 1995 and
608 * creates a SOA or AXFR packet when required by the RFC.
609 */
610 static bool handleIXFR(int fd, const ComboAddress& destination, const MOADNSParser& mdp, const shared_ptr<SOARecordContent>& clientSOA) {
611 vector<std::shared_ptr<ixfrdiff_t>> toSend;
612
613 /* we get a shared pointer of the zone info that we can't modify, ever.
614 A newer one may arise in the meantime, but this one will stay valid
615 until we release it.
616 */
617 auto zoneInfo = getCurrentZoneInfo(mdp.d_qname);
618 if (zoneInfo == nullptr) {
619 return false;
620 }
621
622 uint32_t ourLatestSerial = zoneInfo->soa->d_st.serial;
623
624 if (rfc1982LessThan(ourLatestSerial, clientSOA->d_st.serial) || ourLatestSerial == clientSOA->d_st.serial) {
625 /* RFC 1995 Section 2
626 * If an IXFR query with the same or newer version number than that of
627 * the server is received, it is replied to with a single SOA record of
628 * the server's current version, just as in AXFR.
629 */
630 vector<uint8_t> packet;
631 bool ret = makeSOAPacket(mdp, packet);
632 if (ret) {
633 sendPacketOverTCP(fd, packet);
634 }
635 return ret;
636 }
637
638 // as we use push_back in the updater, we know the vector is sorted as oldest first
639 bool shouldAdd = false;
640 // Get all relevant IXFR differences
641 for (const auto& diff : zoneInfo->ixfrDiffs) {
642 if (shouldAdd) {
643 toSend.push_back(diff);
644 continue;
645 }
646 if (diff->oldSOA->d_st.serial == clientSOA->d_st.serial) {
647 toSend.push_back(diff);
648 // Add all consecutive diffs
649 shouldAdd = true;
650 }
651 }
652
653 if (toSend.empty()) {
654 g_log<<Logger::Warning<<"No IXFR available from serial "<<clientSOA->d_st.serial<<" for zone "<<mdp.d_qname<<", attempting to send AXFR"<<endl;
655 return handleAXFR(fd, mdp);
656 }
657
658 std::vector<std::vector<uint8_t>> packets;
659 for (const auto& diff : toSend) {
660 /* An IXFR packet's ANSWER section looks as follows:
661 * SOA new_serial
662 * SOA old_serial
663 * ... removed records ...
664 * SOA new_serial
665 * ... added records ...
666 * SOA new_serial
667 */
668
669 const auto newSOAPacket = getSOAPacket(mdp, diff->newSOA);
670 const auto oldSOAPacket = getSOAPacket(mdp, diff->oldSOA);
671
672 if (!sendPacketOverTCP(fd, newSOAPacket)) {
673 return false;
674 }
675
676 if (!sendPacketOverTCP(fd, oldSOAPacket)) {
677 return false;
678 }
679
680 if (!sendRecordsOverTCP(fd, mdp, diff->removals)) {
681 return false;
682 }
683
684 if (!sendPacketOverTCP(fd, newSOAPacket)) {
685 return false;
686 }
687
688 if (!sendRecordsOverTCP(fd, mdp, diff->additions)) {
689 return false;
690 }
691
692 if (!sendPacketOverTCP(fd, newSOAPacket)) {
693 return false;
694 }
695 }
696
697 return true;
698 }
699
700 static bool allowedByACL(const ComboAddress& addr) {
701 return g_acl.match(addr);
702 }
703
704 static void handleUDPRequest(int fd, boost::any&) {
705 // TODO make the buffer-size configurable
706 char buf[4096];
707 ComboAddress saddr;
708 socklen_t fromlen = sizeof(saddr);
709 int res = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*) &saddr, &fromlen);
710
711 if (res == 0) {
712 g_log<<Logger::Warning<<"Got an empty message from "<<saddr.toStringWithPort()<<endl;
713 return;
714 }
715
716 if(res < 0) {
717 auto savedErrno = errno;
718 g_log<<Logger::Warning<<"Could not read message from "<<saddr.toStringWithPort()<<": "<<strerror(savedErrno)<<endl;
719 return;
720 }
721
722 if (saddr == ComboAddress("0.0.0.0", 0)) {
723 g_log<<Logger::Warning<<"Could not determine source of message"<<endl;
724 return;
725 }
726
727 if (!allowedByACL(saddr)) {
728 g_log<<Logger::Warning<<"UDP query from "<<saddr.toString()<<" is not allowed, dropping"<<endl;
729 return;
730 }
731
732 MOADNSParser mdp(true, string(buf, res));
733 vector<uint8_t> packet;
734 if (checkQuery(mdp, saddr)) {
735 /* RFC 1995 Section 2
736 * Transport of a query may be by either UDP or TCP. If an IXFR query
737 * is via UDP, the IXFR server may attempt to reply using UDP if the
738 * entire response can be contained in a single DNS packet. If the UDP
739 * reply does not fit, the query is responded to with a single SOA
740 * record of the server's current version to inform the client that a
741 * TCP query should be initiated.
742 *
743 * Let's not complicate this with IXFR over UDP (and looking if we need to truncate etc).
744 * Just send the current SOA and let the client try over TCP
745 */
746 makeSOAPacket(mdp, packet);
747 } else {
748 makeRefusedPacket(mdp, packet);
749 }
750
751 if(sendto(fd, &packet[0], packet.size(), 0, (struct sockaddr*) &saddr, fromlen) < 0) {
752 auto savedErrno = errno;
753 g_log<<Logger::Warning<<"Could not send reply for "<<mdp.d_qname<<"|"<<QType(mdp.d_qtype).getName()<<" to "<<saddr.toStringWithPort()<<": "<<strerror(savedErrno)<<endl;
754 }
755 return;
756 }
757
758 static void handleTCPRequest(int fd, boost::any&) {
759 ComboAddress saddr;
760 int cfd = 0;
761
762 try {
763 cfd = SAccept(fd, saddr);
764 setBlocking(cfd);
765 } catch(runtime_error &e) {
766 g_log<<Logger::Error<<e.what()<<endl;
767 return;
768 }
769
770 if (saddr == ComboAddress("0.0.0.0", 0)) {
771 g_log<<Logger::Warning<<"Could not determine source of message"<<endl;
772 close(cfd);
773 return;
774 }
775
776 if (!allowedByACL(saddr)) {
777 g_log<<Logger::Warning<<"TCP query from "<<saddr.toString()<<" is not allowed, dropping"<<endl;
778 close(cfd);
779 return;
780 }
781
782 {
783 std::lock_guard<std::mutex> lg(g_tcpRequestFDsMutex);
784 g_tcpRequestFDs.push({cfd, saddr});
785 }
786 g_tcpHandlerCV.notify_one();
787 }
788
789 /* Thread to handle TCP traffic
790 */
791 static void tcpWorker(int tid) {
792 setThreadName("ixfrdist/tcpWor");
793 string prefix = "TCP Worker " + std::to_string(tid) + ": ";
794
795 while(true) {
796 g_log<<Logger::Debug<<prefix<<"ready for a new request!"<<endl;
797 std::unique_lock<std::mutex> lk(g_tcpRequestFDsMutex);
798 g_tcpHandlerCV.wait(lk, []{return g_tcpRequestFDs.size() || g_exiting ;});
799 if (g_exiting) {
800 g_log<<Logger::Debug<<prefix<<"Stopping thread"<<endl;
801 break;
802 }
803 g_log<<Logger::Debug<<prefix<<"Going to handle a query"<<endl;
804 auto request = g_tcpRequestFDs.front();
805 g_tcpRequestFDs.pop();
806 lk.unlock();
807
808 int cfd = request.first;
809 ComboAddress saddr = request.second;
810
811 char buf[4096];
812 ssize_t res;
813 try {
814 uint16_t toRead;
815 readn2(cfd, &toRead, sizeof(toRead));
816 toRead = std::min(ntohs(toRead), static_cast<uint16_t>(sizeof(buf)));
817 res = readn2WithTimeout(cfd, &buf, toRead, 2);
818 g_log<<Logger::Debug<<prefix<<"Had message of "<<std::to_string(toRead)<<" bytes from "<<saddr.toStringWithPort()<<endl;
819 } catch (runtime_error &e) {
820 g_log<<Logger::Warning<<prefix<<"Could not read message from "<<saddr.toStringWithPort()<<": "<<e.what()<<endl;
821 close(cfd);
822 continue;
823 }
824
825 try {
826 MOADNSParser mdp(true, string(buf, res));
827
828 if (!checkQuery(mdp, saddr, false, prefix)) {
829 close(cfd);
830 continue;
831 }
832
833 if (mdp.d_qtype == QType::SOA) {
834 vector<uint8_t> packet;
835 bool ret = makeSOAPacket(mdp, packet);
836 if (!ret) {
837 close(cfd);
838 continue;
839 }
840 sendPacketOverTCP(cfd, packet);
841 }
842 else if (mdp.d_qtype == QType::AXFR) {
843 if (!handleAXFR(cfd, mdp)) {
844 close(cfd);
845 continue;
846 }
847 }
848 else if (mdp.d_qtype == QType::IXFR) {
849 /* RFC 1995 section 3:
850 * The IXFR query packet format is the same as that of a normal DNS
851 * query, but with the query type being IXFR and the authority section
852 * containing the SOA record of client's version of the zone.
853 */
854 shared_ptr<SOARecordContent> clientSOA;
855 for (auto &answer : mdp.d_answers) {
856 // from dnsparser.hh:
857 // typedef vector<pair<DNSRecord, uint16_t > > answers_t;
858 if (answer.first.d_type == QType::SOA && answer.first.d_place == DNSResourceRecord::AUTHORITY) {
859 clientSOA = getRR<SOARecordContent>(answer.first);
860 if (clientSOA != nullptr) {
861 break;
862 }
863 }
864 } /* for (auto const &answer : mdp.d_answers) */
865
866 if (clientSOA == nullptr) {
867 g_log<<Logger::Warning<<prefix<<"IXFR request packet did not contain a SOA record in the AUTHORITY section"<<endl;
868 close(cfd);
869 continue;
870 }
871
872 if (!handleIXFR(cfd, saddr, mdp, clientSOA)) {
873 close(cfd);
874 continue;
875 }
876 } /* if (mdp.d_qtype == QType::IXFR) */
877
878 shutdown(cfd, 2);
879 } catch (const MOADNSException &mde) {
880 g_log<<Logger::Warning<<prefix<<"Could not parse DNS packet from "<<saddr.toStringWithPort()<<": "<<mde.what()<<endl;
881 } catch (runtime_error &e) {
882 g_log<<Logger::Warning<<prefix<<"Could not write reply to "<<saddr.toStringWithPort()<<": "<<e.what()<<endl;
883 }
884 // bye!
885 close(cfd);
886
887 if (g_exiting) {
888 break;
889 }
890 }
891 }
892
893 /* Parses the configuration file in configpath into config, adding defaults for
894 * missing parameters (if applicable), returning true if the config file was
895 * good, false otherwise. Will log all issues with the config
896 */
897 static bool parseAndCheckConfig(const string& configpath, YAML::Node& config) {
898 g_log<<Logger::Info<<"Loading configuration file from "<<configpath<<endl;
899 try {
900 config = YAML::LoadFile(configpath);
901 } catch (const runtime_error &e) {
902 g_log<<Logger::Error<<"Unable to load configuration file '"<<configpath<<"': "<<e.what()<<endl;
903 return false;
904 }
905
906 bool retval = true;
907
908 if (config["keep"]) {
909 try {
910 config["keep"].as<uint16_t>();
911 } catch (const runtime_error &e) {
912 g_log<<Logger::Error<<"Unable to read 'keep' value: "<<e.what()<<endl;
913 retval = false;
914 }
915 } else {
916 config["keep"] = 20;
917 }
918
919 if (config["axfr-timeout"]) {
920 try {
921 config["axfr-timeout"].as<uint16_t>();
922 } catch (const runtime_error &e) {
923 g_log<<Logger::Error<<"Unable to read 'axfr-timeout' value: "<<e.what()<<endl;
924 }
925 } else {
926 config["axfr-timeout"] = 20;
927 }
928
929 if (config["failed-soa-retry"]) {
930 try {
931 config["failed-soa-retry"].as<uint16_t>();
932 } catch (const runtime_error &e) {
933 g_log<<Logger::Error<<"Unable to read 'failed-soa-retry' value: "<<e.what()<<endl;
934 }
935 } else {
936 config["failed-soa-retry"] = 30;
937 }
938
939 if (config["tcp-in-threads"]) {
940 try {
941 config["tcp-in-threads"].as<uint16_t>();
942 } catch (const runtime_error &e) {
943 g_log<<Logger::Error<<"Unable to read 'tcp-in-threads' value: "<<e.what()<<endl;
944 }
945 } else {
946 config["tcp-in-threads"] = 10;
947 }
948
949 if (config["listen"]) {
950 try {
951 config["listen"].as<vector<ComboAddress>>();
952 } catch (const runtime_error &e) {
953 g_log<<Logger::Error<<"Unable to read 'listen' value: "<<e.what()<<endl;
954 retval = false;
955 }
956 } else {
957 config["listen"].push_back("127.0.0.1:53");
958 config["listen"].push_back("[::1]:53");
959 }
960
961 if (config["acl"]) {
962 try {
963 config["acl"].as<vector<string>>();
964 } catch (const runtime_error &e) {
965 g_log<<Logger::Error<<"Unable to read 'acl' value: "<<e.what()<<endl;
966 retval = false;
967 }
968 } else {
969 config["acl"].push_back("127.0.0.0/8");
970 config["acl"].push_back("::1/128");
971 }
972
973 if (config["work-dir"]) {
974 try {
975 config["work-dir"].as<string>();
976 } catch(const runtime_error &e) {
977 g_log<<Logger::Error<<"Unable to read 'work-dir' value: "<<e.what()<<endl;
978 retval = false;
979 }
980 } else {
981 char tmp[512];
982 config["work-dir"] = getcwd(tmp, sizeof(tmp)) ? string(tmp) : "";;
983 }
984
985 if (config["uid"]) {
986 try {
987 config["uid"].as<string>();
988 } catch(const runtime_error &e) {
989 g_log<<Logger::Error<<"Unable to read 'uid' value: "<<e.what()<<endl;
990 retval = false;
991 }
992 }
993
994 if (config["gid"]) {
995 try {
996 config["gid"].as<string>();
997 } catch(const runtime_error &e) {
998 g_log<<Logger::Error<<"Unable to read 'gid' value: "<<e.what()<<endl;
999 retval = false;
1000 }
1001 }
1002
1003 if (config["domains"]) {
1004 if (config["domains"].size() == 0) {
1005 g_log<<Logger::Error<<"No domains configured"<<endl;
1006 retval = false;
1007 }
1008 for (auto const &domain : config["domains"]) {
1009 try {
1010 if (!domain["domain"]) {
1011 g_log<<Logger::Error<<"An entry in 'domains' is missing a 'domain' key!"<<endl;
1012 retval = false;
1013 continue;
1014 }
1015 domain["domain"].as<DNSName>();
1016 } catch (const runtime_error &e) {
1017 g_log<<Logger::Error<<"Unable to read domain '"<<domain["domain"].as<string>()<<"': "<<e.what()<<endl;
1018 }
1019 try {
1020 if (!domain["master"]) {
1021 g_log<<Logger::Error<<"Domain '"<<domain["domain"].as<string>()<<"' has no master configured!"<<endl;
1022 retval = false;
1023 continue;
1024 }
1025 domain["master"].as<ComboAddress>();
1026 } catch (const runtime_error &e) {
1027 g_log<<Logger::Error<<"Unable to read domain '"<<domain["domain"].as<string>()<<"' master address: "<<e.what()<<endl;
1028 retval = false;
1029 }
1030 }
1031 } else {
1032 g_log<<Logger::Error<<"No domains configured"<<endl;
1033 retval = false;
1034 }
1035
1036 if (config["compress"]) {
1037 try {
1038 config["compress"].as<bool>();
1039 }
1040 catch (const runtime_error &e) {
1041 g_log<<Logger::Error<<"Unable to read 'compress' value: "<<e.what()<<endl;
1042 retval = false;
1043 }
1044 }
1045 else {
1046 config["compress"] = false;
1047 }
1048
1049 return retval;
1050 }
1051
1052 int main(int argc, char** argv) {
1053 g_log.setLoglevel(Logger::Notice);
1054 g_log.toConsole(Logger::Notice);
1055 g_log.setPrefixed(true);
1056 g_log.disableSyslog(true);
1057 g_log.setTimestamps(false);
1058 po::variables_map g_vm;
1059 try {
1060 po::options_description desc("IXFR distribution tool");
1061 desc.add_options()
1062 ("help", "produce help message")
1063 ("version", "Display the version of ixfrdist")
1064 ("verbose", "Be verbose")
1065 ("debug", "Be even more verbose")
1066 ("config", po::value<string>()->default_value(SYSCONFDIR + string("/ixfrdist.yml")), "Configuration file to use")
1067 ;
1068
1069 po::store(po::command_line_parser(argc, argv).options(desc).run(), g_vm);
1070 po::notify(g_vm);
1071
1072 if (g_vm.count("help") > 0) {
1073 usage(desc);
1074 return EXIT_SUCCESS;
1075 }
1076
1077 if (g_vm.count("version") > 0) {
1078 cout<<"ixfrdist "<<VERSION<<endl;
1079 return EXIT_SUCCESS;
1080 }
1081 } catch (po::error &e) {
1082 g_log<<Logger::Error<<e.what()<<". See `ixfrdist --help` for valid options"<<endl;
1083 return(EXIT_FAILURE);
1084 }
1085
1086 bool had_error = false;
1087
1088 if (g_vm.count("verbose")) {
1089 g_log.setLoglevel(Logger::Info);
1090 g_log.toConsole(Logger::Info);
1091 }
1092
1093 if (g_vm.count("debug") > 0) {
1094 g_log.setLoglevel(Logger::Debug);
1095 g_log.toConsole(Logger::Debug);
1096 }
1097
1098 g_log<<Logger::Notice<<"IXFR distributor version "<<VERSION<<" starting up!"<<endl;
1099
1100 YAML::Node config;
1101 if (!parseAndCheckConfig(g_vm["config"].as<string>(), config)) {
1102 // parseAndCheckConfig already logged whatever was wrong
1103 return EXIT_FAILURE;
1104 }
1105
1106 /* From hereon out, we known that all the values in config are valid. */
1107
1108 for (auto const &domain : config["domains"]) {
1109 set<ComboAddress> s;
1110 s.insert(domain["master"].as<ComboAddress>());
1111 g_domainConfigs[domain["domain"].as<DNSName>()].masters = s;
1112 }
1113
1114 for (const auto &addr : config["acl"].as<vector<string>>()) {
1115 try {
1116 g_acl.addMask(addr);
1117 } catch (const NetmaskException &e) {
1118 g_log<<Logger::Error<<e.reason<<endl;
1119 had_error = true;
1120 }
1121 }
1122 g_log<<Logger::Notice<<"ACL set to "<<g_acl.toString()<<"."<<endl;
1123
1124 if (config["compress"]) {
1125 g_compress = config["compress"].as<bool>();
1126 if (g_compress) {
1127 g_log<<Logger::Notice<<"Record compression is enabled."<<endl;
1128 }
1129 }
1130
1131 FDMultiplexer* fdm = FDMultiplexer::getMultiplexerSilent();
1132 if (fdm == nullptr) {
1133 g_log<<Logger::Error<<"Could not enable a multiplexer for the listen sockets!"<<endl;
1134 return EXIT_FAILURE;
1135 }
1136
1137 set<int> allSockets;
1138 for (const auto& addr : config["listen"].as<vector<ComboAddress>>()) {
1139 for (const auto& stype : {SOCK_DGRAM, SOCK_STREAM}) {
1140 try {
1141 int s = SSocket(addr.sin4.sin_family, stype, 0);
1142 setNonBlocking(s);
1143 setReuseAddr(s);
1144 SBind(s, addr);
1145 if (stype == SOCK_STREAM) {
1146 SListen(s, 30); // TODO make this configurable
1147 }
1148 fdm->addReadFD(s, stype == SOCK_DGRAM ? handleUDPRequest : handleTCPRequest);
1149 allSockets.insert(s);
1150 } catch(runtime_error &e) {
1151 g_log<<Logger::Error<<e.what()<<endl;
1152 had_error = true;
1153 continue;
1154 }
1155 }
1156 }
1157
1158 int newgid = 0;
1159
1160 if (config["gid"]) {
1161 string gid = config["gid"].as<string>();
1162 if (!(newgid = atoi(gid.c_str()))) {
1163 struct group *gr = getgrnam(gid.c_str());
1164 if (gr == nullptr) {
1165 g_log<<Logger::Error<<"Can not determine group-id for gid "<<gid<<endl;
1166 had_error = true;
1167 } else {
1168 newgid = gr->gr_gid;
1169 }
1170 }
1171 g_log<<Logger::Notice<<"Dropping effective group-id to "<<newgid<<endl;
1172 if (setgid(newgid) < 0) {
1173 g_log<<Logger::Error<<"Could not set group id to "<<newgid<<": "<<stringerror()<<endl;
1174 had_error = true;
1175 }
1176 }
1177
1178 int newuid = 0;
1179
1180 if (config["uid"]) {
1181 string uid = config["uid"].as<string>();
1182 if (!(newuid = atoi(uid.c_str()))) {
1183 struct passwd *pw = getpwnam(uid.c_str());
1184 if (pw == nullptr) {
1185 g_log<<Logger::Error<<"Can not determine user-id for uid "<<uid<<endl;
1186 had_error = true;
1187 } else {
1188 newuid = pw->pw_uid;
1189 }
1190 }
1191
1192 struct passwd *pw = getpwuid(newuid);
1193 if (pw == nullptr) {
1194 if (setgroups(0, nullptr) < 0) {
1195 g_log<<Logger::Error<<"Unable to drop supplementary gids: "<<stringerror()<<endl;
1196 had_error = true;
1197 }
1198 } else {
1199 if (initgroups(pw->pw_name, newgid) < 0) {
1200 g_log<<Logger::Error<<"Unable to set supplementary groups: "<<stringerror()<<endl;
1201 had_error = true;
1202 }
1203 }
1204
1205 g_log<<Logger::Notice<<"Dropping effective user-id to "<<newuid<<endl;
1206 if (setuid(newuid) < 0) {
1207 g_log<<Logger::Error<<"Could not set user id to "<<newuid<<": "<<stringerror()<<endl;
1208 had_error = true;
1209 }
1210 }
1211
1212 if (had_error) {
1213 // We have already sent the errors to stderr, just die
1214 return EXIT_FAILURE;
1215 }
1216
1217 // It all starts here
1218 signal(SIGTERM, handleSignal);
1219 signal(SIGINT, handleSignal);
1220 signal(SIGPIPE, SIG_IGN);
1221
1222 // Init the things we need
1223 reportAllTypes();
1224
1225 dns_random_init();
1226
1227 std::thread ut(updateThread,
1228 config["work-dir"].as<string>(),
1229 config["keep"].as<uint16_t>(),
1230 config["axfr-timeout"].as<uint16_t>(),
1231 config["failed-soa-retry"].as<uint16_t>());
1232
1233 vector<std::thread> tcpHandlers;
1234 tcpHandlers.reserve(config["tcp-in-threads"].as<uint16_t>());
1235 for (size_t i = 0; i < tcpHandlers.capacity(); ++i) {
1236 tcpHandlers.push_back(std::thread(tcpWorker, i));
1237 }
1238
1239 struct timeval now;
1240 for(;;) {
1241 gettimeofday(&now, 0);
1242 fdm->run(&now);
1243 if (g_exiting) {
1244 g_log<<Logger::Debug<<"Closing listening sockets"<<endl;
1245 for (const int& fd : allSockets) {
1246 try {
1247 closesocket(fd);
1248 } catch(PDNSException &e) {
1249 g_log<<Logger::Error<<e.reason<<endl;
1250 }
1251 }
1252 break;
1253 }
1254 }
1255 g_log<<Logger::Debug<<"Waiting for al threads to stop"<<endl;
1256 g_tcpHandlerCV.notify_all();
1257 ut.join();
1258 for (auto &t : tcpHandlers) {
1259 t.join();
1260 }
1261 g_log<<Logger::Notice<<"IXFR distributor stopped"<<endl;
1262 return EXIT_SUCCESS;
1263 }