Update pingcheck to work with cmake 3.28
[pingcheck] / src / dns / dnscache.cpp
... / ...
CommitLineData
1/*
2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
4
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
7
8 As a special exception, if other files instantiate templates or use macros
9 or inline functions from this file, or you compile this file and link it
10 with other works to produce a work based on this file, this file
11 does not by itself cause the resulting work to be covered
12 by the GNU General Public License.
13
14 However the source code for this file must still be made available
15 in accordance with section (3) of the GNU General Public License.
16
17 This exception does not invalidate any other reasons why a work based
18 on this file might be covered by the GNU General Public License.
19
20 Christian Herdtweck, Intra2net AG 2015
21 */
22
23#include "dns/dnscache.h"
24
25#include <sstream>
26#include <fstream>
27#include <logfunc.hpp>
28#include <filefunc.hxx> // I2n::file_exists
29#include <tmpfstream.hpp>
30#include <boost/foreach.hpp>
31#include <boost/bind.hpp>
32#include <boost/asio/placeholders.hpp>
33#include <boost/serialization/serialization.hpp>
34#include <boost/serialization/map.hpp>
35#include <boost/serialization/string.hpp>
36#include <boost/serialization/vector.hpp>
37#include <boost/archive/xml_oarchive.hpp>
38#include <boost/archive/xml_iarchive.hpp>
39#include <boost/date_time/gregorian/gregorian_types.hpp>
40
41#include "dns/dnsmaster.h"
42
43using boost::bind;
44using boost::posix_time::seconds;
45using I2n::Logger::GlobalLogger;
46
47
48namespace Config
49{
50 const int SAVE_TIMER_SECONDS = 60;
51 const int CACHE_TIME_WARP_THRESH_MINS = 10;
52 const int CACHE_REMOVE_OUTDATED_DAYS = 60;
53}
54
55
56// -----------------------------------------------------------------------------
57// DNS Cache constructor / destructor
58// -----------------------------------------------------------------------------
59
60const string DnsCache::DoNotUseCacheFile = "do not use cache file!";
61
62DnsCache::DnsCache(const IoServiceItem &io_serv,
63 const std::string &cache_file,
64 const uint32_t min_time_between_resolves)
65 : IpCache()
66 , CnameCache()
67 , SaveTimer( *io_serv )
68 , CacheFile( cache_file )
69 , HasChanged( false )
70 , MinTimeBetweenResolves( min_time_between_resolves )
71{
72 // load cache from file
73 load_from_cachefile();
74
75 // schedule next save
76 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
77 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
78 boost::asio::placeholders::error ) );
79}
80
81
82DnsCache::~DnsCache()
83{
84 GlobalLogger.info() << "DnsCache: being destructed";
85
86 // save one last time without re-scheduling the next save
87 save_to_cachefile();
88
89 // cancel save timer
90 SaveTimer.cancel();
91}
92
93
94// -----------------------------------------------------------------------------
95// LOAD / SAVE
96// -----------------------------------------------------------------------------
97
98void DnsCache::schedule_save(const boost::system::error_code &error)
99{
100 if ( error == boost::asio::error::operation_aborted ) // cancelled
101 {
102 GlobalLogger.info() << "DnsCache: SaveTimer was cancelled "
103 << "--> no save and no re-schedule of saving!";
104 return;
105 }
106 else if (error)
107 {
108 GlobalLogger.info() << "DnsCache: Received error " << error
109 << " in schedule_save "
110 << "--> no save now but re-schedule saving";
111 }
112 else
113 save_to_cachefile();
114
115 // schedule next save
116 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
117 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
118 boost::asio::placeholders::error ) );
119}
120
121void DnsCache::save_to_cachefile()
122{
123 if (!HasChanged)
124 GlobalLogger.info() << "DnsCache: skip saving because has not changed";
125 else if (CacheFile.empty())
126 GlobalLogger.info()
127 << "DnsCache: skip saving because file name empty!";
128 else if (CacheFile == DoNotUseCacheFile)
129 GlobalLogger.info() << "DnsCache: configured not to use cache file";
130 else
131 {
132 try
133 {
134 // clean up: remove very old entries
135 remove_old_entries();
136
137 // remember time of save
138 std::string cache_save_time_str = boost::posix_time::to_iso_string(
139 boost::posix_time::second_clock::universal_time() );
140
141 I2n::tmpofcopystream ofs( CacheFile.c_str() );
142 boost::archive::xml_oarchive oa(ofs);
143 oa & BOOST_SERIALIZATION_NVP(IpCache);
144 oa & BOOST_SERIALIZATION_NVP(CnameCache);
145 oa & BOOST_SERIALIZATION_NVP(cache_save_time_str);
146 GlobalLogger.info() << "DnsCache: saved to cache file "
147 << CacheFile;
148
149 HasChanged = false;
150 }
151 catch (std::exception &exc)
152 {
153 GlobalLogger.warning() << "DnsCache: Saving failed: " << exc.what();
154 }
155 }
156}
157
158void DnsCache::load_from_cachefile()
159{
160 if (CacheFile.empty())
161 GlobalLogger.info()
162 << "DnsCache: cannot load because cache file name is empty!";
163 else if (CacheFile == DoNotUseCacheFile)
164 GlobalLogger.info() << "DnsCache: configured not to use cache file";
165 else if ( !I2n::file_exists(CacheFile) )
166 GlobalLogger.warning() << "DnsCache: cannot load because cache file "
167 << CacheFile << " does not exist!";
168 else
169 {
170 try
171 {
172 std::ifstream ifs( CacheFile.c_str() );
173 boost::archive::xml_iarchive ia(ifs);
174
175 ip_map_type new_IpCache;
176 cname_map_type new_CnameCache;
177 std::string cache_save_time_str;
178
179 ia & BOOST_SERIALIZATION_NVP(new_IpCache);
180 ia & BOOST_SERIALIZATION_NVP(new_CnameCache);
181 ia & BOOST_SERIALIZATION_NVP(cache_save_time_str);
182
183 const boost::posix_time::ptime cache_save_time
184 = boost::posix_time::from_iso_string(cache_save_time_str);
185 GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
186
187 // atomic switch over
188 IpCache.swap(new_IpCache);
189 CnameCache.swap(new_CnameCache);
190
191 check_timestamps(cache_save_time);
192 }
193 catch (boost::archive::archive_exception &exc)
194 {
195 GlobalLogger.warning()
196 << "DnsCache: archive exception loading from " << CacheFile
197 << ": " << exc.what();
198 }
199 catch (std::exception &exc)
200 {
201 GlobalLogger.warning() << "DnsCache: exception while loading from "
202 << CacheFile << ": " << exc.what();
203 }
204 }
205}
206
207
208/**
209 * @brief check that loaded cache really is from the past
210 *
211 * Added this to avoid trouble in case the system time is changed into the past.
212 * In that case would have TTLs here in cache that are valid much too long.
213 *
214 * Therefore check SaveCacheTime and if that is in the future, set all TTLs to 0
215 *
216 * @returns true if had to re-set timestamps
217 */
218bool DnsCache::check_timestamps(const boost::posix_time::ptime &cache_save_time)
219{
220 // check if CacheSaveTime is in the future
221 boost::posix_time::ptime now
222 = boost::posix_time::second_clock::universal_time()
223 + boost::posix_time::minutes(Config::CACHE_TIME_WARP_THRESH_MINS);
224 if (now > cache_save_time)
225 { // Cache was saved in the past -- everything alright
226 return false;
227 }
228
229 GlobalLogger.warning() << "DnsCache: loaded cache from the future (saved "
230 << cache_save_time << ")! Resetting all TTLs to 0.";
231
232 // reset TTLs in IP cache
233 BOOST_FOREACH( ip_map_type::value_type &key_and_ip, IpCache )
234 {
235 BOOST_FOREACH( HostAddress &address, key_and_ip.second )
236 address.get_ttl().set_value(0);
237 }
238
239 // reset TTLs in CNAME cache
240 BOOST_FOREACH( cname_map_type::value_type &key_and_cname, CnameCache )
241 key_and_cname.second.Ttl.set_value(0);
242
243 HasChanged = true;
244
245 //debug_print();
246
247 return true;
248}
249
250
251/**
252 * @brief remove entries from cache that are older than a certain threshold
253 *
254 * this also removes entres from IpCache with no IPs
255 */
256void DnsCache::remove_old_entries()
257{
258 boost::posix_time::ptime thresh
259 = boost::posix_time::second_clock::universal_time()
260 - boost::gregorian::days( Config::CACHE_REMOVE_OUTDATED_DAYS );
261
262 // IP cache
263 {
264 ip_map_type::iterator it = IpCache.begin();
265 ip_map_type::iterator it_end = IpCache.end();
266 bool some_ip_up_to_date;
267 while (it != it_end)
268 {
269 some_ip_up_to_date = false;
270 BOOST_FOREACH( const HostAddress &address, (*it).second )
271 {
272 if ( ! address.get_ttl().was_set_before(thresh) )
273 {
274 some_ip_up_to_date = true;
275 break;
276 }
277 }
278
279 if ( ! some_ip_up_to_date )
280 {
281 GlobalLogger.debug() << "DnsCache: Removing empty/outdated IP "
282 << "list for " << (*it).first.first;
283 IpCache.erase( (*it++).first );
284 }
285 else
286 ++it;
287 }
288 }
289
290 // CNAME cache
291 {
292 cname_map_type::iterator it = CnameCache.begin();
293 cname_map_type::iterator it_end = CnameCache.end();
294 while (it != it_end)
295 {
296 if ( (*it).second.Ttl.was_set_before( thresh ) )
297 {
298 GlobalLogger.debug() << "DnsCache: Removing outdated CNAME for "
299 << (*it).first;
300 CnameCache.erase( (*it++).first );
301 }
302 else
303 ++it;
304 }
305 }
306}
307
308// -----------------------------------------------------------------------------
309// UPDATE
310// -----------------------------------------------------------------------------
311
312/*
313 * warn if hostname is empty and remove trailing dot
314 * also warn if protocol is neither IPv4 nor IPv6
315 */
316ip_map_key_type DnsCache::key_for_ips(const std::string &hostname,
317 const DnsIpProtocol &protocol) const
318{
319 if (hostname.empty())
320 {
321 GlobalLogger.info() << "DnsCache: empty host!";
322 return ip_map_key_type("", DNS_IPALL);
323 }
324 if (protocol == DNS_IPALL)
325 {
326 GlobalLogger.info() << "DnsCache: neither IPv4 nor v6!";
327 return ip_map_key_type("", DNS_IPALL);
328 }
329
330 // check whether last character is a dot
331 if (hostname.rfind('.') == hostname.length()-1)
332 return ip_map_key_type( hostname.substr(0, hostname.length()-1),
333 protocol );
334 else
335 return ip_map_key_type( hostname,
336 protocol );
337}
338
339
340void DnsCache::update(const std::string &hostname,
341 const DnsIpProtocol &protocol,
342 const HostAddressVec &new_ips)
343{
344 // check for valid input arguments
345 ip_map_key_type key = key_for_ips(hostname, protocol);
346 if ( key.first.empty() )
347 return;
348
349 // ensure that there is never IP and CNAME for the same host
350 if ( !get_cname(hostname).Host.empty() )
351 {
352 GlobalLogger.info() << "DnsCache: Saving IPs for " << key.first
353 << " removes CNAME to " << get_cname(hostname).Host << "!";
354 update(hostname, Cname()); // overwrite with "empty" cname
355 }
356
357 // ensure min ttl of MinTimeBetweenResolves
358 HostAddressVec ips_checked;
359 BOOST_FOREACH( const HostAddress &addr, new_ips )
360 {
361 if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
362 {
363 GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
364 << key.first << " from " << addr.get_ttl().get_value() << "s to "
365 << MinTimeBetweenResolves << "s because was too short";
366 ips_checked.push_back( HostAddress( addr.get_ip(),
367 MinTimeBetweenResolves) );
368 }
369 else
370 ips_checked.push_back(addr);
371 }
372
373 // write IPs into one log line
374 stringstream log_temp;
375 log_temp << "DnsCache: update IPs for " << key.first << " to "
376 << ips_checked.size() << "-list: ";
377 BOOST_FOREACH( const HostAddress &ip, ips_checked )
378 log_temp << ip.get_ip() << ", ";
379 GlobalLogger.notice() << log_temp.str();
380
381 IpCache[key] = ips_checked;
382 HasChanged = true;
383}
384
385
386/*
387 * warn if hostname is empty and remove trailing dot
388 */
389cname_map_key_type DnsCache::key_for_cname(const std::string &hostname) const
390{
391 if (hostname.empty())
392 {
393 GlobalLogger.info() << "DnsCache: empty host!";
394 return "";
395 }
396
397 // check whether last character is a dot
398 if (hostname.rfind('.') == hostname.length()-1)
399 return hostname.substr(0, hostname.length()-1);
400 else
401 return hostname;
402}
403
404
405void DnsCache::update(const std::string &hostname,
406 const Cname &cname)
407{
408 // check for valid input arguments
409 cname_map_key_type key = key_for_cname(hostname);
410 if ( key.empty() )
411 return;
412
413 // ensure that there is never IP and CNAME for the same host
414 int n_ips = get_ips(hostname, DNS_IPv4).size()
415 + get_ips(hostname, DNS_IPv6).size();
416 if ( n_ips > 0 )
417 {
418 GlobalLogger.info() << "DnsCache: Saving IPs for " << key
419 << " removes CNAME to " << get_cname(hostname).Host << "!";
420 GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
421 << " removes " << n_ips << " IPs for same host!";
422 update(hostname, DNS_IPv4, HostAddressVec());
423 update(hostname, DNS_IPv6, HostAddressVec());
424 }
425
426 // remove possible trailing dot from cname's target host
427 Cname to_save = Cname(key_for_cname(cname.Host), // implicit cast to string
428 cname.Ttl);
429
430 // ensure min ttl of MinTimeBetweenResolves
431 if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
432 {
433 GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
434 << key << " from " << to_save.Ttl.get_value() << "s to "
435 << MinTimeBetweenResolves << "s because was too short";
436 to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
437 }
438
439 GlobalLogger.notice() << "DnsCache: update CNAME for " << key
440 << " to " << to_save.Host;
441 CnameCache[key] = to_save;
442 HasChanged = true;
443}
444
445
446// -----------------------------------------------------------------------------
447// RETRIEVAL
448// -----------------------------------------------------------------------------
449
450/**
451 * @returns empty list if no (up to date) ips for hostname in cache
452 */
453HostAddressVec DnsCache::get_ips(const std::string &hostname,
454 const DnsIpProtocol &protocol,
455 const bool check_up_to_date)
456{
457 ip_map_key_type key = key_for_ips(hostname, protocol);
458 HostAddressVec result = IpCache[key];
459 if (check_up_to_date)
460 {
461 HostAddressVec result_up_to_date;
462 uint32_t threshold = static_cast<uint32_t>(
463 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
464 uint32_t updated_ttl;
465 BOOST_FOREACH( const HostAddress &addr, result )
466 {
467 updated_ttl = addr.get_ttl().get_updated_value();
468 if (updated_ttl > threshold)
469 result_up_to_date.push_back(addr);
470 else
471 GlobalLogger.debug() << "DnsCache: do not return "
472 << addr.get_ip().to_string() << " since TTL "
473 << updated_ttl << "s is out of date (thresh="
474 << threshold << "s)";
475 }
476 result = result_up_to_date;
477 }
478 /*GlobalLogger.debug() << "DnsCache: request IPs for " << key.first
479 << " --> " << result.size() << "-list";
480 BOOST_FOREACH( const HostAddress &addr, result )
481 GlobalLogger.debug() << "DnsCache: " << addr.get_ip().to_string()
482 << " (TTL " << addr.get_ttl().get_updated_value()
483 << "s)"; */
484 return result;
485}
486
487/**
488 * @returns empty cname if no (up to date cname) for hostname in cache
489 */
490Cname DnsCache::get_cname(const std::string &hostname,
491 const bool check_up_to_date)
492{
493 cname_map_key_type key = key_for_cname(hostname);
494 Cname result_obj = CnameCache[key];
495 /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
496 << " --> \"" << result_obj.Host << "\" (TTL "
497 << result_obj.Ttl.get_updated_value() << "s)";*/
498 if (result_obj.Host.empty())
499 return result_obj;
500
501 else if (check_up_to_date)
502 {
503 if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
504 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
505 return result_obj;
506 else
507 {
508 GlobalLogger.debug() << "DnsCache: CNAME is out of date";
509 return Cname(); // same as if there was no cname for hostname
510 }
511 }
512 else
513 return result_obj;
514}
515
516// underlying assumption in this function: for a hostname, the cache has either
517// a list of IPs saved or a cname saved, but never both
518HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
519 const DnsIpProtocol &protocol,
520 const bool check_up_to_date)
521{
522 std::string current_host = hostname;
523 Cname current_cname;
524 HostAddressVec result = get_ips(current_host, protocol, check_up_to_date);
525 int n_recursions = 0;
526 uint32_t min_cname_ttl = 0xffff; // largest possible unsigned 4-byte value
527 int max_recursion_count = DnsMaster::get_instance()
528 ->get_max_recursion_count();
529 while ( result.empty() )
530 {
531 current_cname = get_cname(current_host, check_up_to_date);
532 if (current_cname.Host.empty())
533 break; // no ips (since result.empty()) and no cname
534 // --> will return empty result
535
536 current_host = current_cname.Host;
537 if (++n_recursions >= max_recursion_count)
538 {
539 GlobalLogger.info() << "DnsCache: reached recursion limit of "
540 << n_recursions << " in recursive IP retrieval of "
541 << hostname << "!";
542 break;
543 }
544 else
545 {
546 min_cname_ttl = min(min_cname_ttl,
547 current_cname.Ttl.get_updated_value());
548 result = get_ips(current_host, protocol, check_up_to_date);
549 }
550 }
551
552 GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
553 << result.size() << "-list after " << n_recursions
554 << " recursions";
555
556 // adjust ttl to min of ttl and min_cname_ttl
557 if (n_recursions > 0)
558 {
559 TimeToLive cname_ttl(min_cname_ttl);
560
561 BOOST_FOREACH( HostAddress &addr, result )
562 {
563 if (addr.get_ttl().get_updated_value() > min_cname_ttl)
564 {
565 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
566 addr.set_ttl(cname_ttl);
567 }
568 }
569 }
570
571 return result;
572}
573
574/**
575 * from a list of CNAMEs find the first one that is out of date or empty
576 *
577 * returns the hostname that is out of date or empty if all CNAMEs are
578 * up-to-date
579 *
580 * required in ResolverBase::get_skipper
581 */
582std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
583 const uint32_t ttl_thresh)
584{
585 std::string first_outdated = hostname;
586 Cname cname;
587 int n_recursions = 0;
588 int max_recursion_count = DnsMaster::get_instance()
589 ->get_max_recursion_count();
590 while (true)
591 {
592 if (++n_recursions >= max_recursion_count)
593 {
594 GlobalLogger.info() << "DnsCache: reached recursion limit of "
595 << n_recursions << " in search of outdated CNAMEs for "
596 << hostname << "!";
597 return first_outdated; // not really out of date but currently
598 } // our best guess
599
600 cname = get_cname(first_outdated);
601 if (cname.Host.empty())
602 // reached end of cname list --> everything was up-to-date
603 return "";
604 else if (cname.Ttl.get_updated_value() > ttl_thresh)
605 // cname is up to date --> continue looking
606 first_outdated = cname.Host;
607 else
608 // cname is out of date --> return its target
609 return cname.Host;
610 }
611 // reach this point only if cname chain does not end with an IP
612 // --> all are up-to-date
613 return "";
614}
615
616std::string DnsCache::get_cname_chain_str(const std::string &hostname)
617{
618 std::stringstream temp;
619 temp << hostname;
620 std::string current_host = hostname;
621 Cname current_cname;
622 int n_recursions = 0;
623 int max_recursion_count = DnsMaster::get_instance()
624 ->get_max_recursion_count();
625 while (true)
626 {
627 if (n_recursions >= max_recursion_count)
628 {
629 temp << "...";
630 break;
631 }
632
633 current_cname = get_cname(current_host, false);
634 if (current_cname.Host.empty())
635 break;
636 else
637 {
638 current_host = current_cname.Host;
639 temp << "-->" << current_host;
640 ++n_recursions;
641 }
642 }
643 return temp.str();
644}
645
646
647// -----------------------------------------------------------------------------
648// OTHER
649// -----------------------------------------------------------------------------
650void DnsCache::debug_print() const
651{
652 GlobalLogger.debug() << "DnsCache: IP Cache contents:";
653 stringstream log_temp;
654 BOOST_FOREACH( const ip_map_type::value_type &key_and_ip, IpCache )
655 {
656 // write IPs into one log line
657 log_temp.str("");
658 log_temp << "DnsCache: " << key_and_ip.first.first << ": \t "
659 << key_and_ip.second.size() << "-list ";
660 BOOST_FOREACH( const HostAddress &ip, key_and_ip.second )
661 log_temp << ip.get_ip() << "+" << ip.get_ttl().get_updated_value()
662 << "s, ";
663 GlobalLogger.debug() << log_temp.str();
664 }
665
666 GlobalLogger.debug() << "DnsCache: CNAME Cache contents:";
667 BOOST_FOREACH( const cname_map_type::value_type &key_and_cname, CnameCache )
668 GlobalLogger.debug() << "DnsCache: " << key_and_cname.first << ": \t "
669 << key_and_cname.second.Host << "+"
670 << key_and_cname.second.Ttl.get_updated_value()
671 << "s";
672}