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