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