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