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