secured DnsCache against time warp-related errors
[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 30#include <boost/bind.hpp>
96779587 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{
77319ba8
CH
48 const int SAVE_TIMER_SECONDS = 60;
49 const int CACHE_TIME_WARP_THRESH_HOURS = 24;
96779587
CH
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
77319ba8 92 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
96779587
CH
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{
96779587
CH
116 if ( error == boost::asio::error::operation_aborted ) // cancelled
117 {
49b82a1d
CH
118 GlobalLogger.info() << "DnsCache: SaveTimer was cancelled "
119 << "--> no save and no re-schedule of saving!";
96779587
CH
120 return;
121 }
122 else if (error)
123 {
49b82a1d
CH
124 GlobalLogger.info() << "DnsCache: Received error " << error
125 << " in schedule_save "
126 << "--> no save now but re-schedule saving";
96779587
CH
127 }
128 else
129 save_to_cachefile();
130
131 // schedule next save
77319ba8 132 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
96779587
CH
133 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
134 boost::asio::placeholders::error ) );
135}
136
137void DnsCache::save_to_cachefile()
138{
139 if (!HasChanged)
ad83004d 140 GlobalLogger.info() << "DnsCache: skip saving because has not changed";
946356e1 141 else if (CacheFile.empty())
49b82a1d 142 GlobalLogger.info()
ad83004d 143 << "DnsCache: skip saving because file name empty!";
8d26221d 144 else if (CacheFile == DoNotUseCacheFile)
26b0f687
CH
145 GlobalLogger.info() << "DnsCache: configured not to use cache file";
146 else
946356e1 147 {
26b0f687
CH
148 try
149 {
77319ba8
CH
150 // remember time of save
151 std::string cache_save_time_str = boost::posix_time::to_iso_string(
152 boost::posix_time::second_clock::universal_time() );
153
26b0f687
CH
154 std::ofstream ofs( CacheFile.c_str() );
155 boost::archive::xml_oarchive oa(ofs);
26b0f687
CH
156 oa & BOOST_SERIALIZATION_NVP(IpCache);
157 oa & BOOST_SERIALIZATION_NVP(CnameCache);
77319ba8 158 oa & BOOST_SERIALIZATION_NVP(cache_save_time_str);
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
77319ba8
CH
188 std::string cache_save_time_str;
189
26b0f687
CH
190 ia & BOOST_SERIALIZATION_NVP(IpCache);
191 ia & BOOST_SERIALIZATION_NVP(CnameCache);
77319ba8
CH
192 ia & BOOST_SERIALIZATION_NVP(cache_save_time_str);
193
194 boost::posix_time::ptime cache_save_time
195 = boost::posix_time::from_iso_string(cache_save_time_str);
26b0f687 196 GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
77319ba8
CH
197
198 check_timestamps(cache_save_time);
26b0f687
CH
199 }
200 catch (boost::archive::archive_exception &exc)
201 {
8d26221d
CH
202 GlobalLogger.warning()
203 << "DnsCache: archive exception loading from " << CacheFile
204 << ": " << exc.what();
26b0f687
CH
205 }
206 catch (std::exception &exc)
207 {
208 GlobalLogger.warning() << "DnsCache: exception while loading from "
209 << CacheFile << ": " << exc.what();
210 }
946356e1 211 }
96779587
CH
212}
213
26b0f687 214
77319ba8
CH
215/**
216 * @brief check that loaded cache really is from the past
217 *
218 * Added this to avoid trouble in case the system time is changed into the past.
219 * In that cast would have TTLs here in cache that are valid much too long.
220 *
221 * Therefore check SaveCacheTime and if that is in the future, set all TTLs to 0
222 *
223 * @returns true if had to re-set timestamps
224 */
225bool DnsCache::check_timestamps(const boost::posix_time::ptime &cache_save_time)
226{
227 // check if CacheSaveTime is in the future
228 boost::posix_time::ptime now
229 = boost::posix_time::second_clock::universal_time()
230 + boost::posix_time::hours( Config::CACHE_TIME_WARP_THRESH_HOURS );
231 if (now > cache_save_time)
232 { // Cache was saved in the past -- everything alright
233 return false;
234 }
235
236 GlobalLogger.warning() << "DnsCache: loaded cache from the future (saved "
237 << cache_save_time << ")! Resetting all TTLs to 0.";
238
239 // reset TTLs in IP cache
240 BOOST_FOREACH( const ip_map_type::value_type key_and_ip, IpCache )
241 {
242 HostAddressVec addr_reset;
243 BOOST_FOREACH( HostAddress address, key_and_ip.second )
244 addr_reset.push_back( HostAddress( address.get_ip(), 0 ) );
245 IpCache[key_and_ip.first] = addr_reset;
246 }
247
248 // reset TTLs in CNAME cache
249 BOOST_FOREACH( const cname_map_type::value_type key_and_cname, CnameCache )
250 CnameCache[key_and_cname.first] = Cname( key_and_cname.second.Host,
251 TimeToLive(0) );
252
253 return true;
254}
255
256
f833126b
CH
257// -----------------------------------------------------------------------------
258// UPDATE
259// -----------------------------------------------------------------------------
260
8f00b3df
CH
261/*
262 * warn if hostname is empty and remove trailing dot
263 * also warn if protocol is neither IPv4 nor IPv6
264 */
265ip_map_key_type DnsCache::key_for_ips(const std::string &hostname,
266 const DnsIpProtocol &protocol) const
96779587 267{
26b0f687
CH
268 if (hostname.empty())
269 {
49b82a1d 270 GlobalLogger.info() << "DnsCache: empty host!";
8f00b3df
CH
271 return ip_map_key_type("", DNS_IPALL);
272 }
273 if (protocol == DNS_IPALL)
274 {
275 GlobalLogger.info() << "DnsCache: neither IPv4 nor v6!";
276 return ip_map_key_type("", DNS_IPALL);
26b0f687
CH
277 }
278
279 // check whether last character is a dot
280 if (hostname.rfind('.') == hostname.length()-1)
8f00b3df
CH
281 return ip_map_key_type( hostname.substr(0, hostname.length()-1),
282 protocol );
26b0f687 283 else
8f00b3df
CH
284 return ip_map_key_type( hostname,
285 protocol );
96779587
CH
286}
287
288
ad83004d 289void DnsCache::update(const std::string &hostname,
8f00b3df 290 const DnsIpProtocol &protocol,
26b0f687 291 const HostAddressVec &new_ips)
ad83004d 292{
8f00b3df
CH
293 // check for valid input arguments
294 ip_map_key_type key = key_for_ips(hostname, protocol);
295 if ( key.first.empty() )
296 return;
297
298 // ensure that there is never IP and CNAME for the same host
26b0f687 299 if ( !get_cname(hostname).Host.empty() )
8f00b3df
CH
300 {
301 GlobalLogger.info() << "DnsCache: Saving IPs for " << key.first
26b0f687
CH
302 << " removes CNAME to " << get_cname(hostname).Host << "!";
303 update(hostname, Cname()); // overwrite with "empty" cname
304 }
8f00b3df 305
f833126b
CH
306 // ensure min ttl of MinTimeBetweenResolves
307 HostAddressVec ips_checked;
308 BOOST_FOREACH( const HostAddress &addr, new_ips )
309 {
310 if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
311 {
312 GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
8f00b3df 313 << key.first << " from " << addr.get_ttl().get_value() << "s to "
f833126b
CH
314 << MinTimeBetweenResolves << "s because was too short";
315 ips_checked.push_back( HostAddress( addr.get_ip(),
316 MinTimeBetweenResolves) );
317 }
318 else
319 ips_checked.push_back(addr);
320 }
321
8f00b3df 322 // write IPs into one log line
f636ba88 323 stringstream log_temp;
8f00b3df 324 log_temp << "DnsCache: update IPs for " << key.first << " to "
f636ba88
CH
325 << ips_checked.size() << "-list: ";
326 BOOST_FOREACH( const HostAddress &ip, ips_checked )
327 log_temp << ip.get_ip() << ", ";
328 GlobalLogger.notice() << log_temp.str();
f833126b
CH
329
330 IpCache[key] = ips_checked;
ad83004d
CH
331 HasChanged = true;
332}
333
334
8f00b3df
CH
335/*
336 * warn if hostname is empty and remove trailing dot
337 */
338cname_map_key_type DnsCache::key_for_cname(const std::string &hostname) const
339{
340 if (hostname.empty())
341 {
342 GlobalLogger.info() << "DnsCache: empty host!";
343 return "";
344 }
345
346 // check whether last character is a dot
347 if (hostname.rfind('.') == hostname.length()-1)
348 return hostname.substr(0, hostname.length()-1);
349 else
350 return hostname;
351}
352
353
dbe986b9 354void DnsCache::update(const std::string &hostname,
26b0f687 355 const Cname &cname)
ad83004d 356{
8f00b3df
CH
357 // check for valid input arguments
358 cname_map_key_type key = key_for_cname(hostname);
359 if ( key.empty() )
360 return;
361
362 // ensure that there is never IP and CNAME for the same host
363 int n_ips = get_ips(hostname, DNS_IPv4).size()
364 + get_ips(hostname, DNS_IPv6).size();
365 if ( n_ips > 0 )
366 {
367 GlobalLogger.info() << "DnsCache: Saving IPs for " << key
368 << " removes CNAME to " << get_cname(hostname).Host << "!";
49b82a1d 369 GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
8f00b3df
CH
370 << " removes " << n_ips << " IPs for same host!";
371 update(hostname, DNS_IPv4, HostAddressVec());
372 update(hostname, DNS_IPv6, HostAddressVec());
ad83004d 373 }
26b0f687 374
8f00b3df
CH
375 // remove possible trailing dot from cname's target host
376 Cname to_save = Cname(key_for_cname(cname.Host), // implicit cast to string
26b0f687
CH
377 cname.Ttl);
378
f833126b
CH
379 // ensure min ttl of MinTimeBetweenResolves
380 if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
381 {
382 GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
8f00b3df 383 << key << " from " << to_save.Ttl.get_value() << "s to "
f833126b
CH
384 << MinTimeBetweenResolves << "s because was too short";
385 to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
386 }
387
f636ba88 388 GlobalLogger.notice() << "DnsCache: update CNAME for " << key
8f00b3df 389 << " to " << to_save.Host;
26b0f687
CH
390 CnameCache[key] = to_save;
391 HasChanged = true;
ad83004d
CH
392}
393
394
f833126b
CH
395// -----------------------------------------------------------------------------
396// RETRIEVAL
397// -----------------------------------------------------------------------------
398
23f51766
CH
399/**
400 * @returns empty list if no (up to date) ips for hostname in cache
401 */
dbe986b9 402HostAddressVec DnsCache::get_ips(const std::string &hostname,
8f00b3df 403 const DnsIpProtocol &protocol,
dbe986b9 404 const bool check_up_to_date)
96779587 405{
8f00b3df 406 ip_map_key_type key = key_for_ips(hostname, protocol);
26b0f687 407 HostAddressVec result = IpCache[key];
dbe986b9
CH
408 if (check_up_to_date)
409 {
410 HostAddressVec result_up_to_date;
23f51766
CH
411 uint32_t threshold = static_cast<uint32_t>(
412 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
26b0f687 413 uint32_t updated_ttl;
dbe986b9
CH
414 BOOST_FOREACH( const HostAddress &addr, result )
415 {
26b0f687
CH
416 updated_ttl = addr.get_ttl().get_updated_value();
417 if (updated_ttl > threshold)
dbe986b9 418 result_up_to_date.push_back(addr);
26b0f687
CH
419 else
420 GlobalLogger.debug() << "DnsCache: do not return "
421 << addr.get_ip().to_string() << " since TTL "
422 << updated_ttl << "s is out of date (thresh="
423 << threshold << "s)";
dbe986b9 424 }
dbe986b9
CH
425 result = result_up_to_date;
426 }
8f00b3df 427 /*GlobalLogger.debug() << "DnsCache: request IPs for " << key.first
26b0f687
CH
428 << " --> " << result.size() << "-list";
429 BOOST_FOREACH( const HostAddress &addr, result )
430 GlobalLogger.debug() << "DnsCache: " << addr.get_ip().to_string()
fd62d09f 431 << " (TTL " << addr.get_ttl().get_updated_value()
2a4dde8b 432 << "s)"; */
dbe986b9 433 return result;
96779587
CH
434}
435
23f51766
CH
436/**
437 * @returns empty cname if no (up to date cname) for hostname in cache
438 */
439Cname DnsCache::get_cname(const std::string &hostname,
440 const bool check_up_to_date)
ad83004d 441{
8f00b3df 442 cname_map_key_type key = key_for_cname(hostname);
26b0f687 443 Cname result_obj = CnameCache[key];
2a4dde8b 444 /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
fd62d09f 445 << " --> \"" << result_obj.Host << "\" (TTL "
2a4dde8b 446 << result_obj.Ttl.get_updated_value() << "s)";*/
23f51766
CH
447 if (result_obj.Host.empty())
448 return result_obj;
449
450 else if (check_up_to_date)
dbe986b9 451 {
26b0f687
CH
452 if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
453 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
23f51766 454 return result_obj;
dbe986b9
CH
455 else
456 {
23f51766
CH
457 GlobalLogger.debug() << "DnsCache: CNAME is out of date";
458 return Cname(); // same as if there was no cname for hostname
dbe986b9
CH
459 }
460 }
461 else
23f51766 462 return result_obj;
ad83004d
CH
463}
464
dbe986b9
CH
465// underlying assumption in this function: for a hostname, the cache has either
466// a list of IPs saved or a cname saved, but never both
467HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
8f00b3df 468 const DnsIpProtocol &protocol,
dbe986b9 469 const bool check_up_to_date)
e18c1337
CH
470{
471 std::string current_host = hostname;
23f51766 472 Cname current_cname;
8f00b3df 473 HostAddressVec result = get_ips(current_host, protocol, check_up_to_date);
dbe986b9 474 int n_recursions = 0;
23f51766 475 uint32_t min_cname_ttl = 0xffff; // largest possible unsigned 4-byte value
cd71d095
CH
476 int max_recursion_count = DnsMaster::get_instance()
477 ->get_max_recursion_count();
e18c1337
CH
478 while ( result.empty() )
479 {
23f51766 480 current_cname = get_cname(current_host, check_up_to_date);
8d26221d 481 if (current_cname.Host.empty())
8f00b3df
CH
482 break; // no ips (since result.empty()) and no cname
483 // --> will return empty result
8d26221d 484
8f00b3df 485 current_host = current_cname.Host;
cd71d095 486 if (++n_recursions >= max_recursion_count)
dbe986b9 487 {
49b82a1d
CH
488 GlobalLogger.info() << "DnsCache: reached recursion limit of "
489 << n_recursions << " in recursive IP retrieval of "
490 << hostname << "!";
dbe986b9
CH
491 break;
492 }
e18c1337 493 else
23f51766 494 {
fd62d09f
CH
495 min_cname_ttl = min(min_cname_ttl,
496 current_cname.Ttl.get_updated_value());
8f00b3df 497 result = get_ips(current_host, protocol, check_up_to_date);
23f51766
CH
498 }
499 }
500
501 GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
502 << result.size() << "-list after " << n_recursions
503 << " recursions";
504
505 // adjust ttl to min of ttl and min_cname_ttl
506 if (n_recursions > 0)
507 {
508 TimeToLive cname_ttl(min_cname_ttl);
509
510 BOOST_FOREACH( HostAddress &addr, result )
511 {
fd62d09f 512 if (addr.get_ttl().get_updated_value() > min_cname_ttl)
8d26221d 513 {
2a4dde8b 514 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
23f51766 515 addr.set_ttl(cname_ttl);
8d26221d 516 }
23f51766 517 }
e18c1337 518 }
23f51766 519
e18c1337
CH
520 return result;
521}
ad83004d 522
23f51766 523/**
9d1b2726
CH
524 * from a list of CNAMEs find the first one that is out of date or empty
525 *
526 * returns the hostname that is out of date or empty if all CNAMEs are
527 * up-to-date
23f51766
CH
528 *
529 * required in ResolverBase::get_skipper
23f51766
CH
530 */
531std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
532 const uint32_t ttl_thresh)
533{
8d26221d 534 std::string first_outdated = hostname;
23f51766
CH
535 Cname cname;
536 int n_recursions = 0;
cd71d095
CH
537 int max_recursion_count = DnsMaster::get_instance()
538 ->get_max_recursion_count();
23f51766
CH
539 while (true)
540 {
cd71d095 541 if (++n_recursions >= max_recursion_count)
23f51766 542 {
49b82a1d
CH
543 GlobalLogger.info() << "DnsCache: reached recursion limit of "
544 << n_recursions << " in search of outdated CNAMEs for "
545 << hostname << "!";
9d1b2726
CH
546 return first_outdated; // not really out of date but currently
547 } // our best guess
23f51766 548
8d26221d
CH
549 cname = get_cname(first_outdated);
550 if (cname.Host.empty())
9d1b2726
CH
551 // reached end of cname list --> everything was up-to-date
552 return "";
23f51766
CH
553 else if (cname.Ttl.get_updated_value() > ttl_thresh)
554 // cname is up to date --> continue looking
8d26221d 555 first_outdated = cname.Host;
23f51766 556 else
9d1b2726
CH
557 // cname is out of date --> return its target
558 return cname.Host;
23f51766 559 }
9d1b2726
CH
560 // reach this point only if cname chain does not end with an IP
561 // --> all are up-to-date
562 return "";
23f51766 563}
e638894d
CH
564
565std::string DnsCache::get_cname_chain_str(const std::string &hostname)
566{
567 std::stringstream temp;
568 temp << hostname;
569 std::string current_host = hostname;
570 Cname current_cname;
571 int n_recursions = 0;
572 int max_recursion_count = DnsMaster::get_instance()
573 ->get_max_recursion_count();
574 while (true)
575 {
576 if (n_recursions >= max_recursion_count)
577 {
578 temp << "...";
579 break;
580 }
581
582 current_cname = get_cname(current_host, false);
583 if (current_cname.Host.empty())
584 break;
585 else
586 {
587 current_host = current_cname.Host;
588 temp << "-->" << current_host;
a176d17a 589 ++n_recursions;
e638894d
CH
590 }
591 }
592 return temp.str();
593}