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