2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
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.
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.
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.
20 Christian Herdtweck, Intra2net AG 2015
23 #include "dns/dnscache.h"
27 #include <logfunc.hpp>
28 #include <filefunc.hxx> // I2n::file_exists
29 #include <boost/foreach.hpp>
30 #include <boost/bind.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 #include <boost/date_time/gregorian/gregorian_types.hpp>
40 #include "dns/dnsmaster.h"
43 using boost::posix_time::seconds;
44 using I2n::Logger::GlobalLogger;
49 const int SAVE_TIMER_SECONDS = 60;
50 const int CACHE_TIME_WARP_THRESH_MINS = 10;
51 const int CACHE_REMOVE_OUTDATED_DAYS = 60;
55 // -----------------------------------------------------------------------------
56 // DNS Cache constructor / destructor
57 // -----------------------------------------------------------------------------
59 const string DnsCache::DoNotUseCacheFile = "do not use cache file!";
61 DnsCache::DnsCache(const IoServiceItem &io_serv,
62 const std::string &cache_file,
63 const uint32_t min_time_between_resolves)
66 , SaveTimer( *io_serv )
67 , CacheFile( cache_file )
69 , MinTimeBetweenResolves( min_time_between_resolves )
71 // load cache from file
72 load_from_cachefile();
75 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
76 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
77 boost::asio::placeholders::error ) );
83 GlobalLogger.info() << "DnsCache: being destructed";
85 // save one last time without re-scheduling the next save
93 // -----------------------------------------------------------------------------
95 // -----------------------------------------------------------------------------
97 void DnsCache::schedule_save(const boost::system::error_code &error)
99 if ( error == boost::asio::error::operation_aborted ) // cancelled
101 GlobalLogger.info() << "DnsCache: SaveTimer was cancelled "
102 << "--> no save and no re-schedule of saving!";
107 GlobalLogger.info() << "DnsCache: Received error " << error
108 << " in schedule_save "
109 << "--> no save now but re-schedule saving";
114 // schedule next save
115 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
116 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
117 boost::asio::placeholders::error ) );
120 void DnsCache::save_to_cachefile()
123 GlobalLogger.info() << "DnsCache: skip saving because has not changed";
124 else if (CacheFile.empty())
126 << "DnsCache: skip saving because file name empty!";
127 else if (CacheFile == DoNotUseCacheFile)
128 GlobalLogger.info() << "DnsCache: configured not to use cache file";
133 // clean up: remove very old entries
134 remove_old_entries();
136 // remember time of save
137 std::string cache_save_time_str = boost::posix_time::to_iso_string(
138 boost::posix_time::second_clock::universal_time() );
140 std::ofstream ofs( CacheFile.c_str() );
141 boost::archive::xml_oarchive oa(ofs);
142 oa & BOOST_SERIALIZATION_NVP(IpCache);
143 oa & BOOST_SERIALIZATION_NVP(CnameCache);
144 oa & BOOST_SERIALIZATION_NVP(cache_save_time_str);
145 GlobalLogger.info() << "DnsCache: saved to cache file "
150 catch (std::exception &exc)
152 GlobalLogger.warning() << "DnsCache: Saving failed: " << exc.what();
157 void DnsCache::load_from_cachefile()
159 if (CacheFile.empty())
161 << "DnsCache: cannot load because cache file name is empty!";
162 else if (CacheFile == DoNotUseCacheFile)
163 GlobalLogger.info() << "DnsCache: configured not to use cache file";
164 else if ( !I2n::file_exists(CacheFile) )
165 GlobalLogger.warning() << "DnsCache: cannot load because cache file "
166 << CacheFile << " does not exist!";
171 std::ifstream ifs( CacheFile.c_str() );
172 boost::archive::xml_iarchive ia(ifs);
174 ip_map_type new_IpCache;
175 cname_map_type new_CnameCache;
176 std::string cache_save_time_str;
178 ia & BOOST_SERIALIZATION_NVP(new_IpCache);
179 ia & BOOST_SERIALIZATION_NVP(new_CnameCache);
180 ia & BOOST_SERIALIZATION_NVP(cache_save_time_str);
182 const boost::posix_time::ptime cache_save_time
183 = boost::posix_time::from_iso_string(cache_save_time_str);
184 GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
186 // atomic switch over
187 IpCache.swap(new_IpCache);
188 CnameCache.swap(new_CnameCache);
190 check_timestamps(cache_save_time);
192 catch (boost::archive::archive_exception &exc)
194 GlobalLogger.warning()
195 << "DnsCache: archive exception loading from " << CacheFile
196 << ": " << exc.what();
198 catch (std::exception &exc)
200 GlobalLogger.warning() << "DnsCache: exception while loading from "
201 << CacheFile << ": " << exc.what();
208 * @brief check that loaded cache really is from the past
210 * Added this to avoid trouble in case the system time is changed into the past.
211 * In that case would have TTLs here in cache that are valid much too long.
213 * Therefore check SaveCacheTime and if that is in the future, set all TTLs to 0
215 * @returns true if had to re-set timestamps
217 bool DnsCache::check_timestamps(const boost::posix_time::ptime &cache_save_time)
219 // check if CacheSaveTime is in the future
220 boost::posix_time::ptime now
221 = boost::posix_time::second_clock::universal_time()
222 + boost::posix_time::minutes(Config::CACHE_TIME_WARP_THRESH_MINS);
223 if (now > cache_save_time)
224 { // Cache was saved in the past -- everything alright
228 GlobalLogger.warning() << "DnsCache: loaded cache from the future (saved "
229 << cache_save_time << ")! Resetting all TTLs to 0.";
231 // reset TTLs in IP cache
232 BOOST_FOREACH( ip_map_type::value_type &key_and_ip, IpCache )
234 BOOST_FOREACH( HostAddress &address, key_and_ip.second )
235 address.get_ttl().set_value(0);
238 // reset TTLs in CNAME cache
239 BOOST_FOREACH( cname_map_type::value_type &key_and_cname, CnameCache )
240 key_and_cname.second.Ttl.set_value(0);
251 * @brief remove entries from cache that are older than a certain threshold
253 * this also removes entres from IpCache with no IPs
255 void DnsCache::remove_old_entries()
257 boost::posix_time::ptime thresh
258 = boost::posix_time::second_clock::universal_time()
259 - boost::gregorian::days( Config::CACHE_REMOVE_OUTDATED_DAYS );
263 ip_map_type::iterator it = IpCache.begin();
264 ip_map_type::iterator it_end = IpCache.end();
265 bool some_ip_up_to_date;
268 some_ip_up_to_date = false;
269 BOOST_FOREACH( const HostAddress &address, (*it).second )
271 if ( ! address.get_ttl().was_set_before(thresh) )
273 some_ip_up_to_date = true;
278 if ( ! some_ip_up_to_date )
280 GlobalLogger.debug() << "DnsCache: Removing empty/outdated IP "
281 << "list for " << (*it).first.first;
282 IpCache.erase( (*it++).first );
291 cname_map_type::iterator it = CnameCache.begin();
292 cname_map_type::iterator it_end = CnameCache.end();
295 if ( (*it).second.Ttl.was_set_before( thresh ) )
297 GlobalLogger.debug() << "DnsCache: Removing outdated CNAME for "
299 CnameCache.erase( (*it++).first );
307 // -----------------------------------------------------------------------------
309 // -----------------------------------------------------------------------------
312 * warn if hostname is empty and remove trailing dot
313 * also warn if protocol is neither IPv4 nor IPv6
315 ip_map_key_type DnsCache::key_for_ips(const std::string &hostname,
316 const DnsIpProtocol &protocol) const
318 if (hostname.empty())
320 GlobalLogger.info() << "DnsCache: empty host!";
321 return ip_map_key_type("", DNS_IPALL);
323 if (protocol == DNS_IPALL)
325 GlobalLogger.info() << "DnsCache: neither IPv4 nor v6!";
326 return ip_map_key_type("", DNS_IPALL);
329 // check whether last character is a dot
330 if (hostname.rfind('.') == hostname.length()-1)
331 return ip_map_key_type( hostname.substr(0, hostname.length()-1),
334 return ip_map_key_type( hostname,
339 void DnsCache::update(const std::string &hostname,
340 const DnsIpProtocol &protocol,
341 const HostAddressVec &new_ips)
343 // check for valid input arguments
344 ip_map_key_type key = key_for_ips(hostname, protocol);
345 if ( key.first.empty() )
348 // ensure that there is never IP and CNAME for the same host
349 if ( !get_cname(hostname).Host.empty() )
351 GlobalLogger.info() << "DnsCache: Saving IPs for " << key.first
352 << " removes CNAME to " << get_cname(hostname).Host << "!";
353 update(hostname, Cname()); // overwrite with "empty" cname
356 // ensure min ttl of MinTimeBetweenResolves
357 HostAddressVec ips_checked;
358 BOOST_FOREACH( const HostAddress &addr, new_ips )
360 if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
362 GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
363 << key.first << " from " << addr.get_ttl().get_value() << "s to "
364 << MinTimeBetweenResolves << "s because was too short";
365 ips_checked.push_back( HostAddress( addr.get_ip(),
366 MinTimeBetweenResolves) );
369 ips_checked.push_back(addr);
372 // write IPs into one log line
373 stringstream log_temp;
374 log_temp << "DnsCache: update IPs for " << key.first << " to "
375 << ips_checked.size() << "-list: ";
376 BOOST_FOREACH( const HostAddress &ip, ips_checked )
377 log_temp << ip.get_ip() << ", ";
378 GlobalLogger.notice() << log_temp.str();
380 IpCache[key] = ips_checked;
386 * warn if hostname is empty and remove trailing dot
388 cname_map_key_type DnsCache::key_for_cname(const std::string &hostname) const
390 if (hostname.empty())
392 GlobalLogger.info() << "DnsCache: empty host!";
396 // check whether last character is a dot
397 if (hostname.rfind('.') == hostname.length()-1)
398 return hostname.substr(0, hostname.length()-1);
404 void DnsCache::update(const std::string &hostname,
407 // check for valid input arguments
408 cname_map_key_type key = key_for_cname(hostname);
412 // ensure that there is never IP and CNAME for the same host
413 int n_ips = get_ips(hostname, DNS_IPv4).size()
414 + get_ips(hostname, DNS_IPv6).size();
417 GlobalLogger.info() << "DnsCache: Saving IPs for " << key
418 << " removes CNAME to " << get_cname(hostname).Host << "!";
419 GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
420 << " removes " << n_ips << " IPs for same host!";
421 update(hostname, DNS_IPv4, HostAddressVec());
422 update(hostname, DNS_IPv6, HostAddressVec());
425 // remove possible trailing dot from cname's target host
426 Cname to_save = Cname(key_for_cname(cname.Host), // implicit cast to string
429 // ensure min ttl of MinTimeBetweenResolves
430 if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
432 GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
433 << key << " from " << to_save.Ttl.get_value() << "s to "
434 << MinTimeBetweenResolves << "s because was too short";
435 to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
438 GlobalLogger.notice() << "DnsCache: update CNAME for " << key
439 << " to " << to_save.Host;
440 CnameCache[key] = to_save;
445 // -----------------------------------------------------------------------------
447 // -----------------------------------------------------------------------------
450 * @returns empty list if no (up to date) ips for hostname in cache
452 HostAddressVec DnsCache::get_ips(const std::string &hostname,
453 const DnsIpProtocol &protocol,
454 const bool check_up_to_date)
456 ip_map_key_type key = key_for_ips(hostname, protocol);
457 HostAddressVec result = IpCache[key];
458 if (check_up_to_date)
460 HostAddressVec result_up_to_date;
461 uint32_t threshold = static_cast<uint32_t>(
462 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
463 uint32_t updated_ttl;
464 BOOST_FOREACH( const HostAddress &addr, result )
466 updated_ttl = addr.get_ttl().get_updated_value();
467 if (updated_ttl > threshold)
468 result_up_to_date.push_back(addr);
470 GlobalLogger.debug() << "DnsCache: do not return "
471 << addr.get_ip().to_string() << " since TTL "
472 << updated_ttl << "s is out of date (thresh="
473 << threshold << "s)";
475 result = result_up_to_date;
477 /*GlobalLogger.debug() << "DnsCache: request IPs for " << key.first
478 << " --> " << result.size() << "-list";
479 BOOST_FOREACH( const HostAddress &addr, result )
480 GlobalLogger.debug() << "DnsCache: " << addr.get_ip().to_string()
481 << " (TTL " << addr.get_ttl().get_updated_value()
487 * @returns empty cname if no (up to date cname) for hostname in cache
489 Cname DnsCache::get_cname(const std::string &hostname,
490 const bool check_up_to_date)
492 cname_map_key_type key = key_for_cname(hostname);
493 Cname result_obj = CnameCache[key];
494 /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
495 << " --> \"" << result_obj.Host << "\" (TTL "
496 << result_obj.Ttl.get_updated_value() << "s)";*/
497 if (result_obj.Host.empty())
500 else if (check_up_to_date)
502 if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
503 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
507 GlobalLogger.debug() << "DnsCache: CNAME is out of date";
508 return Cname(); // same as if there was no cname for hostname
515 // underlying assumption in this function: for a hostname, the cache has either
516 // a list of IPs saved or a cname saved, but never both
517 HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
518 const DnsIpProtocol &protocol,
519 const bool check_up_to_date)
521 std::string current_host = hostname;
523 HostAddressVec result = get_ips(current_host, protocol, check_up_to_date);
524 int n_recursions = 0;
525 uint32_t min_cname_ttl = 0xffff; // largest possible unsigned 4-byte value
526 int max_recursion_count = DnsMaster::get_instance()
527 ->get_max_recursion_count();
528 while ( result.empty() )
530 current_cname = get_cname(current_host, check_up_to_date);
531 if (current_cname.Host.empty())
532 break; // no ips (since result.empty()) and no cname
533 // --> will return empty result
535 current_host = current_cname.Host;
536 if (++n_recursions >= max_recursion_count)
538 GlobalLogger.info() << "DnsCache: reached recursion limit of "
539 << n_recursions << " in recursive IP retrieval of "
545 min_cname_ttl = min(min_cname_ttl,
546 current_cname.Ttl.get_updated_value());
547 result = get_ips(current_host, protocol, check_up_to_date);
551 GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
552 << result.size() << "-list after " << n_recursions
555 // adjust ttl to min of ttl and min_cname_ttl
556 if (n_recursions > 0)
558 TimeToLive cname_ttl(min_cname_ttl);
560 BOOST_FOREACH( HostAddress &addr, result )
562 if (addr.get_ttl().get_updated_value() > min_cname_ttl)
564 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
565 addr.set_ttl(cname_ttl);
574 * from a list of CNAMEs find the first one that is out of date or empty
576 * returns the hostname that is out of date or empty if all CNAMEs are
579 * required in ResolverBase::get_skipper
581 std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
582 const uint32_t ttl_thresh)
584 std::string first_outdated = hostname;
586 int n_recursions = 0;
587 int max_recursion_count = DnsMaster::get_instance()
588 ->get_max_recursion_count();
591 if (++n_recursions >= max_recursion_count)
593 GlobalLogger.info() << "DnsCache: reached recursion limit of "
594 << n_recursions << " in search of outdated CNAMEs for "
596 return first_outdated; // not really out of date but currently
599 cname = get_cname(first_outdated);
600 if (cname.Host.empty())
601 // reached end of cname list --> everything was up-to-date
603 else if (cname.Ttl.get_updated_value() > ttl_thresh)
604 // cname is up to date --> continue looking
605 first_outdated = cname.Host;
607 // cname is out of date --> return its target
610 // reach this point only if cname chain does not end with an IP
611 // --> all are up-to-date
615 std::string DnsCache::get_cname_chain_str(const std::string &hostname)
617 std::stringstream temp;
619 std::string current_host = hostname;
621 int n_recursions = 0;
622 int max_recursion_count = DnsMaster::get_instance()
623 ->get_max_recursion_count();
626 if (n_recursions >= max_recursion_count)
632 current_cname = get_cname(current_host, false);
633 if (current_cname.Host.empty())
637 current_host = current_cname.Host;
638 temp << "-->" << current_host;
646 // -----------------------------------------------------------------------------
648 // -----------------------------------------------------------------------------
649 void DnsCache::debug_print() const
651 GlobalLogger.debug() << "DnsCache: IP Cache contents:";
652 stringstream log_temp;
653 BOOST_FOREACH( const ip_map_type::value_type &key_and_ip, IpCache )
655 // write IPs into one log line
657 log_temp << "DnsCache: " << key_and_ip.first.first << ": \t "
658 << key_and_ip.second.size() << "-list ";
659 BOOST_FOREACH( const HostAddress &ip, key_and_ip.second )
660 log_temp << ip.get_ip() << "+" << ip.get_ttl().get_updated_value()
662 GlobalLogger.debug() << log_temp.str();
665 GlobalLogger.debug() << "DnsCache: CNAME Cache contents:";
666 BOOST_FOREACH( const cname_map_type::value_type &key_and_cname, CnameCache )
667 GlobalLogger.debug() << "DnsCache: " << key_and_cname.first << ": \t "
668 << key_and_cname.second.Host << "+"
669 << key_and_cname.second.Ttl.get_updated_value()