0e9a270ec5e7fea8024b1188dbfadd4d379de21b
[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 <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
43 using boost::bind;
44 using boost::posix_time::seconds;
45 using I2n::Logger::GlobalLogger;
46
47
48 namespace 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
60 const string DnsCache::DoNotUseCacheFile = "do not use cache file!";
61
62 DnsCache::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
82 DnsCache::~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
98 void 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
121 void 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
158 void 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  */
218 bool 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  */
256 void 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  */
316 ip_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
340 void 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  */
389 cname_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
405 void 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  */
453 HostAddressVec 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  */
490 Cname 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
518 HostAddressVec 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  */
582 std::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
616 std::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 // -----------------------------------------------------------------------------
650 void 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 }