ensured LogPrefix is used in DNS and PingScheduler; shortened lines; remove vim end...
[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_value() << "s)";
281     return result;
282 }
283
284 /**
285  * @returns empty cname if no (up to date cname) for hostname in cache
286  */
287 Cname DnsCache::get_cname(const std::string &hostname,
288                           const bool check_up_to_date)
289 {
290     std::string key = key_for_hostname(hostname);
291     Cname result_obj = CnameCache[key];
292     GlobalLogger.debug() << "DnsCache: request CNAME for " << key
293                          << " --> \"" << result_obj.Host << "\"";
294     if (result_obj.Host.empty())
295         return result_obj;
296
297     else if (check_up_to_date)
298     {
299         if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
300                    DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
301             return result_obj;
302         else
303         {
304             GlobalLogger.debug() << "DnsCache: CNAME is out of date";
305             return Cname();    // same as if there was no cname for hostname
306         }
307     }
308     else
309         return result_obj;
310 }
311
312 // underlying assumption in this function: for a hostname, the cache has either
313 // a list of IPs saved or a cname saved, but never both
314 HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
315                                            const bool check_up_to_date)
316 {
317     std::string current_host = hostname;
318     Cname current_cname;
319     HostAddressVec result = get_ips(current_host);
320     int n_recursions = 0;
321     uint32_t min_cname_ttl = 0xffff;   // largest possible unsigned 4-byte value
322     while ( result.empty() )
323     {
324         current_cname = get_cname(current_host, check_up_to_date);
325         if (current_cname.Host.empty())
326             break;
327
328         current_host = key_for_hostname(current_cname.Host);
329         if (++n_recursions >= Config::MaxRetrievalRecursions)
330         {
331             GlobalLogger.warning() << "DnsCache: reached recursion limit of "
332                 << n_recursions << " in recursive IP retrieval!";
333             break;
334         }
335         else
336         {
337             min_cname_ttl = min(min_cname_ttl, current_cname.Ttl.get_value());
338             result = get_ips(current_host, check_up_to_date);
339         }
340     }
341
342     GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
343                          << result.size() << "-list after " << n_recursions
344                          << " recursions";
345
346     // adjust ttl to min of ttl and min_cname_ttl
347     if (n_recursions > 0)
348     {
349         TimeToLive cname_ttl(min_cname_ttl);
350
351         BOOST_FOREACH( HostAddress &addr, result )
352         {
353             if (addr.get_ttl().get_value() > min_cname_ttl)
354             {
355                 GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
356                 addr.set_ttl(cname_ttl);
357             }
358         }
359     }
360
361     return result;
362 }
363
364 /**
365  * from a list of CNAMEs find the first one that is out of date or empty
366  *
367  * returns the hostname that is out of date or empty if all CNAMEs are
368  *   up-to-date
369  *
370  * required in ResolverBase::get_skipper
371  */
372 std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
373                                                const uint32_t ttl_thresh)
374 {
375     std::string first_outdated = hostname;
376     Cname cname;
377     int n_recursions = 0;
378     while (true)
379     {
380         if (++n_recursions >= Config::MaxRetrievalRecursions)
381         {
382             GlobalLogger.warning() << "DnsCache: reached recursion limit of "
383                 << n_recursions << " in search of outdated CNAMEs!";
384             return first_outdated;   // not really out of date but currently
385         }                            // our best guess
386
387         cname = get_cname(first_outdated);
388         if (cname.Host.empty())
389             // reached end of cname list --> everything was up-to-date
390             return "";
391         else if (cname.Ttl.get_updated_value() > ttl_thresh)
392             // cname is up to date --> continue looking
393             first_outdated = cname.Host;
394         else
395             // cname is out of date --> return its target
396             return cname.Host;
397     }
398     // reach this point only if cname chain does not end with an IP
399     // --> all are up-to-date
400     return "";
401 }