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