re-raise a little output in cache: state newly acquired IPs and CNAMEs
[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 // 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.info() << "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.info() << "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     stringstream log_temp;
255     log_temp << "DnsCache: update IPs for " << key << " to "
256              << ips_checked.size() << "-list: ";
257     BOOST_FOREACH( const HostAddress &ip, ips_checked )
258         log_temp << ip.get_ip() << ", ";
259     GlobalLogger.notice() << log_temp.str();
260
261     IpCache[key] = ips_checked;
262     HasChanged = true;
263 }
264
265
266 void DnsCache::update(const std::string &hostname,
267                       const Cname &cname)
268 {
269     std::string key = key_for_hostname(hostname);
270     if ( !get_ips(hostname).empty() )
271     {   // ensure that there is never IP and CNAME for the same host
272         GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
273             << " removes " << get_ips(hostname).size() << " IPs for same host!";
274         update(hostname, HostAddressVec());   // overwrite with empty IP list
275     }
276
277     // remove possible trailing dot from cname
278     Cname to_save = Cname(key_for_hostname(cname.Host),
279                           cname.Ttl);
280
281     // ensure min ttl of MinTimeBetweenResolves
282     if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
283     {
284         GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
285             << hostname << " from " << to_save.Ttl.get_value() << "s to "
286             << MinTimeBetweenResolves << "s because was too short";
287         to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
288     }
289
290     GlobalLogger.notice() << "DnsCache: update CNAME for " << key
291                         << " to " << to_save.Host;
292     CnameCache[key] = to_save;
293     HasChanged = true;
294 }
295
296
297 // -----------------------------------------------------------------------------
298 // RETRIEVAL
299 // -----------------------------------------------------------------------------
300
301 /**
302  * @returns empty list if no (up to date) ips for hostname in cache
303  */
304 HostAddressVec DnsCache::get_ips(const std::string &hostname,
305                                  const bool check_up_to_date)
306 {
307     std::string key = key_for_hostname(hostname);
308     HostAddressVec result = IpCache[key];
309     if (check_up_to_date)
310     {
311         HostAddressVec result_up_to_date;
312         uint32_t threshold = static_cast<uint32_t>(
313                 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
314         uint32_t updated_ttl;
315         BOOST_FOREACH( const HostAddress &addr, result )
316         {
317             updated_ttl = addr.get_ttl().get_updated_value();
318             if (updated_ttl > threshold)
319                 result_up_to_date.push_back(addr);
320             else
321                 GlobalLogger.debug() << "DnsCache: do not return "
322                     << addr.get_ip().to_string() << " since TTL "
323                     << updated_ttl << "s is out of date (thresh="
324                     << threshold << "s)";
325         }
326         result = result_up_to_date;
327     }
328     /*GlobalLogger.debug() << "DnsCache: request IPs for " << key
329                          << " --> " << result.size() << "-list";
330     BOOST_FOREACH( const HostAddress &addr, result )
331         GlobalLogger.debug() << "DnsCache:    " << addr.get_ip().to_string()
332                              << " (TTL " << addr.get_ttl().get_updated_value()
333                              << "s)"; */
334     return result;
335 }
336
337 /**
338  * @returns empty cname if no (up to date cname) for hostname in cache
339  */
340 Cname DnsCache::get_cname(const std::string &hostname,
341                           const bool check_up_to_date)
342 {
343     std::string key = key_for_hostname(hostname);
344     Cname result_obj = CnameCache[key];
345     /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
346                          << " --> \"" << result_obj.Host << "\" (TTL "
347                          << result_obj.Ttl.get_updated_value() << "s)";*/
348     if (result_obj.Host.empty())
349         return result_obj;
350
351     else if (check_up_to_date)
352     {
353         if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
354                    DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
355             return result_obj;
356         else
357         {
358             GlobalLogger.debug() << "DnsCache: CNAME is out of date";
359             return Cname();    // same as if there was no cname for hostname
360         }
361     }
362     else
363         return result_obj;
364 }
365
366 // underlying assumption in this function: for a hostname, the cache has either
367 // a list of IPs saved or a cname saved, but never both
368 HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
369                                            const bool check_up_to_date)
370 {
371     std::string current_host = hostname;
372     Cname current_cname;
373     HostAddressVec result = get_ips(current_host, check_up_to_date);
374     int n_recursions = 0;
375     uint32_t min_cname_ttl = 0xffff;   // largest possible unsigned 4-byte value
376     int max_recursion_count = DnsMaster::get_instance()
377                                        ->get_max_recursion_count();
378     while ( result.empty() )
379     {
380         current_cname = get_cname(current_host, check_up_to_date);
381         if (current_cname.Host.empty())
382             break;
383
384         current_host = key_for_hostname(current_cname.Host);
385         if (++n_recursions >= max_recursion_count)
386         {
387             GlobalLogger.info() << "DnsCache: reached recursion limit of "
388                 << n_recursions << " in recursive IP retrieval of "
389                 << hostname << "!";
390             break;
391         }
392         else
393         {
394             min_cname_ttl = min(min_cname_ttl,
395                                 current_cname.Ttl.get_updated_value());
396             result = get_ips(current_host, check_up_to_date);
397         }
398     }
399
400     GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
401                          << result.size() << "-list after " << n_recursions
402                          << " recursions";
403
404     // adjust ttl to min of ttl and min_cname_ttl
405     if (n_recursions > 0)
406     {
407         TimeToLive cname_ttl(min_cname_ttl);
408
409         BOOST_FOREACH( HostAddress &addr, result )
410         {
411             if (addr.get_ttl().get_updated_value() > min_cname_ttl)
412             {
413                 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
414                 addr.set_ttl(cname_ttl);
415             }
416         }
417     }
418
419     return result;
420 }
421
422 /**
423  * from a list of CNAMEs find the first one that is out of date or empty
424  *
425  * returns the hostname that is out of date or empty if all CNAMEs are
426  *   up-to-date
427  *
428  * required in ResolverBase::get_skipper
429  */
430 std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
431                                                const uint32_t ttl_thresh)
432 {
433     std::string first_outdated = hostname;
434     Cname cname;
435     int n_recursions = 0;
436     int max_recursion_count = DnsMaster::get_instance()
437                                        ->get_max_recursion_count();
438     while (true)
439     {
440         if (++n_recursions >= max_recursion_count)
441         {
442             GlobalLogger.info() << "DnsCache: reached recursion limit of "
443                 << n_recursions << " in search of outdated CNAMEs for "
444                 << hostname << "!";
445             return first_outdated;   // not really out of date but currently
446         }                            // our best guess
447
448         cname = get_cname(first_outdated);
449         if (cname.Host.empty())
450             // reached end of cname list --> everything was up-to-date
451             return "";
452         else if (cname.Ttl.get_updated_value() > ttl_thresh)
453             // cname is up to date --> continue looking
454             first_outdated = cname.Host;
455         else
456             // cname is out of date --> return its target
457             return cname.Host;
458     }
459     // reach this point only if cname chain does not end with an IP
460     // --> all are up-to-date
461     return "";
462 }