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