added option min-time-between-resolves-option and tests for it
[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     int MaxRetrievalRecursions = 10;
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.error() << "DnsCache: SaveTimer was cancelled "
122                              << "--> no save and no re-schedule of saving!";
123         return;
124     }
125     else if (error)
126     {
127         GlobalLogger.error() << "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.warning()
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.warning()
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 // warn if hostname is empty and remove trailing dot
212 std::string DnsCache::key_for_hostname(const std::string &hostname) const
213 {
214     if (hostname.empty())
215     {
216         GlobalLogger.warning() << "DnsCache: empty host!";
217         return "";
218     }
219
220     // check whether last character is a dot
221     if (hostname.rfind('.') == hostname.length()-1)
222         return hostname.substr(0, hostname.length()-1);
223     else
224         return hostname;
225 }
226
227
228 void DnsCache::update(const std::string &hostname,
229                       const HostAddressVec &new_ips)
230 {
231     std::string key = key_for_hostname(hostname);
232     if ( !get_cname(hostname).Host.empty() )
233     {   // ensure that there is never IP and CNAME for the same host
234         GlobalLogger.warning() << "DnsCache: Saving IPs for " << key
235             << " removes CNAME to " << get_cname(hostname).Host << "!";
236         update(hostname, Cname());   // overwrite with "empty" cname
237     }
238     // ensure min ttl of MinTimeBetweenResolves
239     HostAddressVec ips_checked;
240     BOOST_FOREACH( const HostAddress &addr, new_ips )
241     {
242         if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
243         {
244             GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
245                 << hostname << " from " << addr.get_ttl().get_value() << "s to "
246                 << MinTimeBetweenResolves << "s because was too short";
247             ips_checked.push_back( HostAddress( addr.get_ip(),
248                                                 MinTimeBetweenResolves) );
249         }
250         else
251             ips_checked.push_back(addr);
252     }
253
254     GlobalLogger.info() << "DnsCache: update IPs for " << key
255                         << " to " << ips_checked.size() << "-list";
256
257     IpCache[key] = ips_checked;
258     HasChanged = true;
259 }
260
261
262 void DnsCache::update(const std::string &hostname,
263                       const Cname &cname)
264 {
265     std::string key = key_for_hostname(hostname);
266     if ( !get_ips(hostname).empty() )
267     {   // ensure that there is never IP and CNAME for the same host
268         GlobalLogger.warning() << "DnsCache: Saving CNAME for " << key
269             << " removes " << get_ips(hostname).size() << " IPs for same host!";
270         update(hostname, HostAddressVec());   // overwrite with empty IP list
271     }
272
273     // remove possible trailing dot from cname
274     Cname to_save = Cname(key_for_hostname(cname.Host),
275                           cname.Ttl);
276
277     // ensure min ttl of MinTimeBetweenResolves
278     if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
279     {
280         GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
281             << hostname << " from " << to_save.Ttl.get_value() << "s to "
282             << MinTimeBetweenResolves << "s because was too short";
283         to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
284     }
285
286     GlobalLogger.info() << "DnsCache: update CNAME for " << key
287                         << " to " << to_save.Host;
288     CnameCache[key] = to_save;
289     HasChanged = true;
290 }
291
292
293 // -----------------------------------------------------------------------------
294 // RETRIEVAL
295 // -----------------------------------------------------------------------------
296
297 /**
298  * @returns empty list if no (up to date) ips for hostname in cache
299  */
300 HostAddressVec DnsCache::get_ips(const std::string &hostname,
301                                  const bool check_up_to_date)
302 {
303     std::string key = key_for_hostname(hostname);
304     HostAddressVec result = IpCache[key];
305     if (check_up_to_date)
306     {
307         HostAddressVec result_up_to_date;
308         uint32_t threshold = static_cast<uint32_t>(
309                 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
310         uint32_t updated_ttl;
311         BOOST_FOREACH( const HostAddress &addr, result )
312         {
313             updated_ttl = addr.get_ttl().get_updated_value();
314             if (updated_ttl > threshold)
315                 result_up_to_date.push_back(addr);
316             else
317                 GlobalLogger.debug() << "DnsCache: do not return "
318                     << addr.get_ip().to_string() << " since TTL "
319                     << updated_ttl << "s is out of date (thresh="
320                     << threshold << "s)";
321         }
322         result = result_up_to_date;
323     }
324     /*GlobalLogger.debug() << "DnsCache: request IPs for " << key
325                          << " --> " << result.size() << "-list";
326     BOOST_FOREACH( const HostAddress &addr, result )
327         GlobalLogger.debug() << "DnsCache:    " << addr.get_ip().to_string()
328                              << " (TTL " << addr.get_ttl().get_updated_value()
329                              << "s)"; */
330     return result;
331 }
332
333 /**
334  * @returns empty cname if no (up to date cname) for hostname in cache
335  */
336 Cname DnsCache::get_cname(const std::string &hostname,
337                           const bool check_up_to_date)
338 {
339     std::string key = key_for_hostname(hostname);
340     Cname result_obj = CnameCache[key];
341     /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
342                          << " --> \"" << result_obj.Host << "\" (TTL "
343                          << result_obj.Ttl.get_updated_value() << "s)";*/
344     if (result_obj.Host.empty())
345         return result_obj;
346
347     else if (check_up_to_date)
348     {
349         if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
350                    DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
351             return result_obj;
352         else
353         {
354             GlobalLogger.debug() << "DnsCache: CNAME is out of date";
355             return Cname();    // same as if there was no cname for hostname
356         }
357     }
358     else
359         return result_obj;
360 }
361
362 // underlying assumption in this function: for a hostname, the cache has either
363 // a list of IPs saved or a cname saved, but never both
364 HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
365                                            const bool check_up_to_date)
366 {
367     std::string current_host = hostname;
368     Cname current_cname;
369     HostAddressVec result = get_ips(current_host, check_up_to_date);
370     int n_recursions = 0;
371     uint32_t min_cname_ttl = 0xffff;   // largest possible unsigned 4-byte value
372     while ( result.empty() )
373     {
374         current_cname = get_cname(current_host, check_up_to_date);
375         if (current_cname.Host.empty())
376             break;
377
378         current_host = key_for_hostname(current_cname.Host);
379         if (++n_recursions >= Config::MaxRetrievalRecursions)
380         {
381             GlobalLogger.warning() << "DnsCache: reached recursion limit of "
382                 << n_recursions << " in recursive IP retrieval!";
383             break;
384         }
385         else
386         {
387             min_cname_ttl = min(min_cname_ttl,
388                                 current_cname.Ttl.get_updated_value());
389             result = get_ips(current_host, check_up_to_date);
390         }
391     }
392
393     GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
394                          << result.size() << "-list after " << n_recursions
395                          << " recursions";
396
397     // adjust ttl to min of ttl and min_cname_ttl
398     if (n_recursions > 0)
399     {
400         TimeToLive cname_ttl(min_cname_ttl);
401
402         BOOST_FOREACH( HostAddress &addr, result )
403         {
404             if (addr.get_ttl().get_updated_value() > min_cname_ttl)
405             {
406                 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
407                 addr.set_ttl(cname_ttl);
408             }
409         }
410     }
411
412     return result;
413 }
414
415 /**
416  * from a list of CNAMEs find the first one that is out of date or empty
417  *
418  * returns the hostname that is out of date or empty if all CNAMEs are
419  *   up-to-date
420  *
421  * required in ResolverBase::get_skipper
422  */
423 std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
424                                                const uint32_t ttl_thresh)
425 {
426     std::string first_outdated = hostname;
427     Cname cname;
428     int n_recursions = 0;
429     while (true)
430     {
431         if (++n_recursions >= Config::MaxRetrievalRecursions)
432         {
433             GlobalLogger.warning() << "DnsCache: reached recursion limit of "
434                 << n_recursions << " in search of outdated CNAMEs!";
435             return first_outdated;   // not really out of date but currently
436         }                            // our best guess
437
438         cname = get_cname(first_outdated);
439         if (cname.Host.empty())
440             // reached end of cname list --> everything was up-to-date
441             return "";
442         else if (cname.Ttl.get_updated_value() > ttl_thresh)
443             // cname is up to date --> continue looking
444             first_outdated = cname.Host;
445         else
446             // cname is out of date --> return its target
447             return cname.Host;
448     }
449     // reach this point only if cname chain does not end with an IP
450     // --> all are up-to-date
451     return "";
452 }