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