bugfixed erase and re-set of TTLs; changed time warp thresh from 24h to 10mins
[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             std::string cache_save_time_str;
175
176             ia & BOOST_SERIALIZATION_NVP(IpCache);
177             ia & BOOST_SERIALIZATION_NVP(CnameCache);
178             ia & BOOST_SERIALIZATION_NVP(cache_save_time_str);
179
180             boost::posix_time::ptime cache_save_time
181                     = boost::posix_time::from_iso_string(cache_save_time_str);
182             GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
183
184             check_timestamps(cache_save_time);
185         }
186         catch (boost::archive::archive_exception &exc)
187         {
188             GlobalLogger.warning()
189                 << "DnsCache: archive exception loading from " << CacheFile
190                 << ": " << exc.what();
191         }
192         catch (std::exception &exc)
193         {
194             GlobalLogger.warning() << "DnsCache: exception while loading from "
195                                    << CacheFile << ": " << exc.what();
196         }
197     }
198 }
199
200
201 /**
202  * @brief check that loaded cache really is from the past
203  *
204  * Added this to avoid trouble in case the system time is changed into the past.
205  * In that cast would have TTLs here in cache that are valid much too long.
206  *
207  * Therefore check SaveCacheTime and if that is in the future, set all TTLs to 0
208  *
209  * @returns true if had to re-set timestamps
210  */
211 bool DnsCache::check_timestamps(const boost::posix_time::ptime &cache_save_time)
212 {
213     // check if CacheSaveTime is in the future
214     boost::posix_time::ptime now
215              = boost::posix_time::second_clock::universal_time()
216              + boost::posix_time::minutes(Config::CACHE_TIME_WARP_THRESH_MINS);
217     if (now > cache_save_time)
218     {   // Cache was saved in the past -- everything alright
219         return false;
220     }
221
222     GlobalLogger.warning() << "DnsCache: loaded cache from the future (saved "
223                            << cache_save_time << ")! Resetting all TTLs to 0.";
224
225     // reset TTLs in IP cache
226     BOOST_FOREACH( ip_map_type::value_type &key_and_ip, IpCache )
227     {
228         BOOST_FOREACH( HostAddress &address, key_and_ip.second )
229             address.get_ttl().set_value(0);
230     }
231
232     // reset TTLs in CNAME cache
233     BOOST_FOREACH( cname_map_type::value_type &key_and_cname, CnameCache )
234         key_and_cname.second.Ttl.set_value(0);
235
236     HasChanged = true;
237
238     //debug_print();
239
240     return true;
241 }
242
243
244 /**
245  * @brief remove entries from cache that are older than a certain threshold
246  *
247  * this also removes entres from IpCache with no IPs
248  */
249 void DnsCache::remove_old_entries()
250 {
251     boost::posix_time::ptime thresh
252              = boost::posix_time::second_clock::universal_time()
253              - boost::gregorian::days( Config::CACHE_REMOVE_OUTDATED_DAYS );
254
255     // IP cache
256     {
257         ip_map_type::iterator it = IpCache.begin();
258         ip_map_type::iterator it_end = IpCache.end();
259         bool some_ip_up_to_date;
260         while (it != it_end)
261         {
262             some_ip_up_to_date = false;
263             BOOST_FOREACH( const HostAddress &address, (*it).second )
264             {
265                 if ( ! address.get_ttl().was_set_before(thresh) )
266                 {
267                     some_ip_up_to_date = true;
268                     break;
269                 }
270             }
271
272             if ( ! some_ip_up_to_date )
273             {
274                 GlobalLogger.debug() << "DnsCache: Removing empty/outdated IP "
275                                      << "list for " << (*it).first.first;
276                 IpCache.erase( (*it++).first );
277             }
278             else
279                 ++it;
280         }
281     }
282
283     // CNAME cache
284     {
285         cname_map_type::iterator it = CnameCache.begin();
286         cname_map_type::iterator it_end = CnameCache.end();
287         while (it != it_end)
288         {
289             if ( (*it).second.Ttl.was_set_before( thresh ) )
290             {
291                 GlobalLogger.debug() << "DnsCache: Removing outdated CNAME for "
292                                      << (*it).first;
293                 CnameCache.erase( (*it++).first );
294             }
295             else
296                 ++it;
297         }
298     }
299 }
300
301 // -----------------------------------------------------------------------------
302 // UPDATE
303 // -----------------------------------------------------------------------------
304
305 /*
306  * warn if hostname is empty and remove trailing dot
307  * also warn if protocol is neither IPv4 nor IPv6
308  */
309 ip_map_key_type DnsCache::key_for_ips(const std::string &hostname,
310                                    const DnsIpProtocol &protocol) const
311 {
312     if (hostname.empty())
313     {
314         GlobalLogger.info() << "DnsCache: empty host!";
315         return ip_map_key_type("", DNS_IPALL);
316     }
317     if (protocol == DNS_IPALL)
318     {
319         GlobalLogger.info() << "DnsCache: neither IPv4 nor v6!";
320         return ip_map_key_type("", DNS_IPALL);
321     }
322
323     // check whether last character is a dot
324     if (hostname.rfind('.') == hostname.length()-1)
325         return ip_map_key_type( hostname.substr(0, hostname.length()-1),
326                              protocol );
327     else
328         return ip_map_key_type( hostname,
329                              protocol );
330 }
331
332
333 void DnsCache::update(const std::string &hostname,
334                       const DnsIpProtocol &protocol,
335                       const HostAddressVec &new_ips)
336 {
337     // check for valid input arguments
338     ip_map_key_type key = key_for_ips(hostname, protocol);
339     if ( key.first.empty() )
340         return;
341
342     // ensure that there is never IP and CNAME for the same host
343     if ( !get_cname(hostname).Host.empty() )
344     {
345         GlobalLogger.info() << "DnsCache: Saving IPs for " << key.first
346             << " removes CNAME to " << get_cname(hostname).Host << "!";
347         update(hostname, Cname());   // overwrite with "empty" cname
348     }
349
350     // ensure min ttl of MinTimeBetweenResolves
351     HostAddressVec ips_checked;
352     BOOST_FOREACH( const HostAddress &addr, new_ips )
353     {
354         if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
355         {
356             GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
357                 << key.first << " from " << addr.get_ttl().get_value() << "s to "
358                 << MinTimeBetweenResolves << "s because was too short";
359             ips_checked.push_back( HostAddress( addr.get_ip(),
360                                                 MinTimeBetweenResolves) );
361         }
362         else
363             ips_checked.push_back(addr);
364     }
365
366     // write IPs into one log line
367     stringstream log_temp;
368     log_temp << "DnsCache: update IPs for " << key.first << " to "
369              << ips_checked.size() << "-list: ";
370     BOOST_FOREACH( const HostAddress &ip, ips_checked )
371         log_temp << ip.get_ip() << ", ";
372     GlobalLogger.notice() << log_temp.str();
373
374     IpCache[key] = ips_checked;
375     HasChanged = true;
376 }
377
378
379 /*
380  * warn if hostname is empty and remove trailing dot
381  */
382 cname_map_key_type DnsCache::key_for_cname(const std::string &hostname) const
383 {
384     if (hostname.empty())
385     {
386         GlobalLogger.info() << "DnsCache: empty host!";
387         return "";
388     }
389
390     // check whether last character is a dot
391     if (hostname.rfind('.') == hostname.length()-1)
392         return hostname.substr(0, hostname.length()-1);
393     else
394         return hostname;
395 }
396
397
398 void DnsCache::update(const std::string &hostname,
399                       const Cname &cname)
400 {
401     // check for valid input arguments
402     cname_map_key_type key = key_for_cname(hostname);
403     if ( key.empty() )
404         return;
405
406     // ensure that there is never IP and CNAME for the same host
407     int n_ips = get_ips(hostname, DNS_IPv4).size()
408               + get_ips(hostname, DNS_IPv6).size();
409     if ( n_ips > 0 )
410     {
411         GlobalLogger.info() << "DnsCache: Saving IPs for " << key
412             << " removes CNAME to " << get_cname(hostname).Host << "!";
413         GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
414             << " removes " <<  n_ips << " IPs for same host!";
415         update(hostname, DNS_IPv4, HostAddressVec());
416         update(hostname, DNS_IPv6, HostAddressVec());
417     }
418
419     // remove possible trailing dot from cname's target host
420     Cname to_save = Cname(key_for_cname(cname.Host),  // implicit cast to string
421                           cname.Ttl);
422
423     // ensure min ttl of MinTimeBetweenResolves
424     if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
425     {
426         GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
427             << key << " from " << to_save.Ttl.get_value() << "s to "
428             << MinTimeBetweenResolves << "s because was too short";
429         to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
430     }
431
432     GlobalLogger.notice() << "DnsCache: update CNAME for " << key
433                           << " to " << to_save.Host;
434     CnameCache[key] = to_save;
435     HasChanged = true;
436 }
437
438
439 // -----------------------------------------------------------------------------
440 // RETRIEVAL
441 // -----------------------------------------------------------------------------
442
443 /**
444  * @returns empty list if no (up to date) ips for hostname in cache
445  */
446 HostAddressVec DnsCache::get_ips(const std::string &hostname,
447                                  const DnsIpProtocol &protocol,
448                                  const bool check_up_to_date)
449 {
450     ip_map_key_type key = key_for_ips(hostname, protocol);
451     HostAddressVec result = IpCache[key];
452     if (check_up_to_date)
453     {
454         HostAddressVec result_up_to_date;
455         uint32_t threshold = static_cast<uint32_t>(
456                 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
457         uint32_t updated_ttl;
458         BOOST_FOREACH( const HostAddress &addr, result )
459         {
460             updated_ttl = addr.get_ttl().get_updated_value();
461             if (updated_ttl > threshold)
462                 result_up_to_date.push_back(addr);
463             else
464                 GlobalLogger.debug() << "DnsCache: do not return "
465                     << addr.get_ip().to_string() << " since TTL "
466                     << updated_ttl << "s is out of date (thresh="
467                     << threshold << "s)";
468         }
469         result = result_up_to_date;
470     }
471     /*GlobalLogger.debug() << "DnsCache: request IPs for " << key.first
472                          << " --> " << result.size() << "-list";
473     BOOST_FOREACH( const HostAddress &addr, result )
474         GlobalLogger.debug() << "DnsCache:    " << addr.get_ip().to_string()
475                              << " (TTL " << addr.get_ttl().get_updated_value()
476                              << "s)"; */
477     return result;
478 }
479
480 /**
481  * @returns empty cname if no (up to date cname) for hostname in cache
482  */
483 Cname DnsCache::get_cname(const std::string &hostname,
484                           const bool check_up_to_date)
485 {
486     cname_map_key_type key = key_for_cname(hostname);
487     Cname result_obj = CnameCache[key];
488     /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
489                          << " --> \"" << result_obj.Host << "\" (TTL "
490                          << result_obj.Ttl.get_updated_value() << "s)";*/
491     if (result_obj.Host.empty())
492         return result_obj;
493
494     else if (check_up_to_date)
495     {
496         if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
497                    DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
498             return result_obj;
499         else
500         {
501             GlobalLogger.debug() << "DnsCache: CNAME is out of date";
502             return Cname();    // same as if there was no cname for hostname
503         }
504     }
505     else
506         return result_obj;
507 }
508
509 // underlying assumption in this function: for a hostname, the cache has either
510 // a list of IPs saved or a cname saved, but never both
511 HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
512                                            const DnsIpProtocol &protocol,
513                                            const bool check_up_to_date)
514 {
515     std::string current_host = hostname;
516     Cname current_cname;
517     HostAddressVec result = get_ips(current_host, protocol, check_up_to_date);
518     int n_recursions = 0;
519     uint32_t min_cname_ttl = 0xffff;   // largest possible unsigned 4-byte value
520     int max_recursion_count = DnsMaster::get_instance()
521                                        ->get_max_recursion_count();
522     while ( result.empty() )
523     {
524         current_cname = get_cname(current_host, check_up_to_date);
525         if (current_cname.Host.empty())
526             break;   // no ips (since result.empty()) and no cname
527                      // --> will return empty result
528
529         current_host = current_cname.Host;
530         if (++n_recursions >= max_recursion_count)
531         {
532             GlobalLogger.info() << "DnsCache: reached recursion limit of "
533                 << n_recursions << " in recursive IP retrieval of "
534                 << hostname << "!";
535             break;
536         }
537         else
538         {
539             min_cname_ttl = min(min_cname_ttl,
540                                 current_cname.Ttl.get_updated_value());
541             result = get_ips(current_host, protocol, check_up_to_date);
542         }
543     }
544
545     GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
546                          << result.size() << "-list after " << n_recursions
547                          << " recursions";
548
549     // adjust ttl to min of ttl and min_cname_ttl
550     if (n_recursions > 0)
551     {
552         TimeToLive cname_ttl(min_cname_ttl);
553
554         BOOST_FOREACH( HostAddress &addr, result )
555         {
556             if (addr.get_ttl().get_updated_value() > min_cname_ttl)
557             {
558                 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
559                 addr.set_ttl(cname_ttl);
560             }
561         }
562     }
563
564     return result;
565 }
566
567 /**
568  * from a list of CNAMEs find the first one that is out of date or empty
569  *
570  * returns the hostname that is out of date or empty if all CNAMEs are
571  *   up-to-date
572  *
573  * required in ResolverBase::get_skipper
574  */
575 std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
576                                                const uint32_t ttl_thresh)
577 {
578     std::string first_outdated = hostname;
579     Cname cname;
580     int n_recursions = 0;
581     int max_recursion_count = DnsMaster::get_instance()
582                                        ->get_max_recursion_count();
583     while (true)
584     {
585         if (++n_recursions >= max_recursion_count)
586         {
587             GlobalLogger.info() << "DnsCache: reached recursion limit of "
588                 << n_recursions << " in search of outdated CNAMEs for "
589                 << hostname << "!";
590             return first_outdated;   // not really out of date but currently
591         }                            // our best guess
592
593         cname = get_cname(first_outdated);
594         if (cname.Host.empty())
595             // reached end of cname list --> everything was up-to-date
596             return "";
597         else if (cname.Ttl.get_updated_value() > ttl_thresh)
598             // cname is up to date --> continue looking
599             first_outdated = cname.Host;
600         else
601             // cname is out of date --> return its target
602             return cname.Host;
603     }
604     // reach this point only if cname chain does not end with an IP
605     // --> all are up-to-date
606     return "";
607 }
608
609 std::string DnsCache::get_cname_chain_str(const std::string &hostname)
610 {
611     std::stringstream temp;
612     temp << hostname;
613     std::string current_host = hostname;
614     Cname current_cname;
615     int n_recursions = 0;
616     int max_recursion_count = DnsMaster::get_instance()
617                                        ->get_max_recursion_count();
618     while (true)
619     {
620         if (n_recursions >= max_recursion_count)
621         {
622             temp << "...";
623             break;
624         }
625
626         current_cname = get_cname(current_host, false);
627         if (current_cname.Host.empty())
628             break;
629         else
630         {
631             current_host = current_cname.Host;
632             temp << "-->" << current_host;
633             ++n_recursions;
634         }
635     }
636     return temp.str();
637 }
638
639
640 // -----------------------------------------------------------------------------
641 // OTHER
642 // -----------------------------------------------------------------------------
643 void DnsCache::debug_print() const
644 {
645     GlobalLogger.debug() << "DnsCache: IP Cache contents:";
646     stringstream log_temp;
647     BOOST_FOREACH( const ip_map_type::value_type &key_and_ip, IpCache )
648     {
649         // write IPs into one log line
650         log_temp.str("");
651         log_temp << "DnsCache: " << key_and_ip.first.first << ": \t "
652                  << key_and_ip.second.size() << "-list ";
653         BOOST_FOREACH( const HostAddress &ip, key_and_ip.second )
654             log_temp << ip.get_ip() << "+" << ip.get_ttl().get_updated_value()
655                      << "s, ";
656         GlobalLogger.debug() << log_temp.str();
657     }
658
659     GlobalLogger.debug() << "DnsCache: CNAME Cache contents:";
660     BOOST_FOREACH( const cname_map_type::value_type &key_and_cname, CnameCache )
661         GlobalLogger.debug() << "DnsCache: " << key_and_cname.first << ": \t "
662                              << key_and_cname.second.Host << "+"
663                              << key_and_cname.second.Ttl.get_updated_value()
664                              << "s";
665 }