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