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