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