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 <tmpfstream.hpp>
30 #include <boost/foreach.hpp>
31 #include <boost/bind.hpp>
32 #include <boost/asio/placeholders.hpp>
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>
39 #include <boost/date_time/gregorian/gregorian_types.hpp>
41 #include "dns/dnsmaster.h"
44 using boost::posix_time::seconds;
45 using I2n::Logger::GlobalLogger;
50 const int SAVE_TIMER_SECONDS = 60;
51 const int CACHE_TIME_WARP_THRESH_MINS = 10;
52 const int CACHE_REMOVE_OUTDATED_DAYS = 60;
56 // -----------------------------------------------------------------------------
57 // DNS Cache constructor / destructor
58 // -----------------------------------------------------------------------------
60 const string DnsCache::DoNotUseCacheFile = "do not use cache file!";
62 DnsCache::DnsCache(const IoServiceItem &io_serv,
63 const std::string &cache_file,
64 const uint32_t min_time_between_resolves)
67 , SaveTimer( *io_serv )
68 , CacheFile( cache_file )
70 , MinTimeBetweenResolves( min_time_between_resolves )
72 // load cache from file
73 load_from_cachefile();
76 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
77 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
78 boost::asio::placeholders::error ) );
84 GlobalLogger.info() << "DnsCache: being destructed";
86 // save one last time without re-scheduling the next save
94 // -----------------------------------------------------------------------------
96 // -----------------------------------------------------------------------------
98 void DnsCache::schedule_save(const boost::system::error_code &error)
100 if ( error == boost::asio::error::operation_aborted ) // cancelled
102 GlobalLogger.info() << "DnsCache: SaveTimer was cancelled "
103 << "--> no save and no re-schedule of saving!";
108 GlobalLogger.info() << "DnsCache: Received error " << error
109 << " in schedule_save "
110 << "--> no save now but re-schedule saving";
115 // schedule next save
116 (void) SaveTimer.expires_from_now( seconds( Config::SAVE_TIMER_SECONDS ) );
117 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
118 boost::asio::placeholders::error ) );
121 void DnsCache::save_to_cachefile()
124 GlobalLogger.info() << "DnsCache: skip saving because has not changed";
125 else if (CacheFile.empty())
127 << "DnsCache: skip saving because file name empty!";
128 else if (CacheFile == DoNotUseCacheFile)
129 GlobalLogger.info() << "DnsCache: configured not to use cache file";
134 // clean up: remove very old entries
135 remove_old_entries();
137 // remember time of save
138 std::string cache_save_time_str = boost::posix_time::to_iso_string(
139 boost::posix_time::second_clock::universal_time() );
141 I2n::tmpofcopystream ofs( CacheFile.c_str() );
142 boost::archive::xml_oarchive oa(ofs);
143 oa & BOOST_SERIALIZATION_NVP(IpCache);
144 oa & BOOST_SERIALIZATION_NVP(CnameCache);
145 oa & BOOST_SERIALIZATION_NVP(cache_save_time_str);
146 GlobalLogger.info() << "DnsCache: saved to cache file "
151 catch (std::exception &exc)
153 GlobalLogger.warning() << "DnsCache: Saving failed: " << exc.what();
158 void DnsCache::load_from_cachefile()
160 if (CacheFile.empty())
162 << "DnsCache: cannot load because cache file name is empty!";
163 else if (CacheFile == DoNotUseCacheFile)
164 GlobalLogger.info() << "DnsCache: configured not to use cache file";
165 else if ( !I2n::file_exists(CacheFile) )
166 GlobalLogger.warning() << "DnsCache: cannot load because cache file "
167 << CacheFile << " does not exist!";
172 std::ifstream ifs( CacheFile.c_str() );
173 boost::archive::xml_iarchive ia(ifs);
175 ip_map_type new_IpCache;
176 cname_map_type new_CnameCache;
177 std::string cache_save_time_str;
179 ia & BOOST_SERIALIZATION_NVP(new_IpCache);
180 ia & BOOST_SERIALIZATION_NVP(new_CnameCache);
181 ia & BOOST_SERIALIZATION_NVP(cache_save_time_str);
183 const boost::posix_time::ptime cache_save_time
184 = boost::posix_time::from_iso_string(cache_save_time_str);
185 GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
187 // atomic switch over
188 IpCache.swap(new_IpCache);
189 CnameCache.swap(new_CnameCache);
191 check_timestamps(cache_save_time);
193 catch (boost::archive::archive_exception &exc)
195 GlobalLogger.warning()
196 << "DnsCache: archive exception loading from " << CacheFile
197 << ": " << exc.what();
199 catch (std::exception &exc)
201 GlobalLogger.warning() << "DnsCache: exception while loading from "
202 << CacheFile << ": " << exc.what();
209 * @brief check that loaded cache really is from the past
211 * Added this to avoid trouble in case the system time is changed into the past.
212 * In that case would have TTLs here in cache that are valid much too long.
214 * Therefore check SaveCacheTime and if that is in the future, set all TTLs to 0
216 * @returns true if had to re-set timestamps
218 bool DnsCache::check_timestamps(const boost::posix_time::ptime &cache_save_time)
220 // check if CacheSaveTime is in the future
221 boost::posix_time::ptime now
222 = boost::posix_time::second_clock::universal_time()
223 + boost::posix_time::minutes(Config::CACHE_TIME_WARP_THRESH_MINS);
224 if (now > cache_save_time)
225 { // Cache was saved in the past -- everything alright
229 GlobalLogger.warning() << "DnsCache: loaded cache from the future (saved "
230 << cache_save_time << ")! Resetting all TTLs to 0.";
232 // reset TTLs in IP cache
233 BOOST_FOREACH( ip_map_type::value_type &key_and_ip, IpCache )
235 BOOST_FOREACH( HostAddress &address, key_and_ip.second )
236 address.get_ttl().set_value(0);
239 // reset TTLs in CNAME cache
240 BOOST_FOREACH( cname_map_type::value_type &key_and_cname, CnameCache )
241 key_and_cname.second.Ttl.set_value(0);
252 * @brief remove entries from cache that are older than a certain threshold
254 * this also removes entres from IpCache with no IPs
256 void DnsCache::remove_old_entries()
258 boost::posix_time::ptime thresh
259 = boost::posix_time::second_clock::universal_time()
260 - boost::gregorian::days( Config::CACHE_REMOVE_OUTDATED_DAYS );
264 ip_map_type::iterator it = IpCache.begin();
265 ip_map_type::iterator it_end = IpCache.end();
266 bool some_ip_up_to_date;
269 some_ip_up_to_date = false;
270 BOOST_FOREACH( const HostAddress &address, (*it).second )
272 if ( ! address.get_ttl().was_set_before(thresh) )
274 some_ip_up_to_date = true;
279 if ( ! some_ip_up_to_date )
281 GlobalLogger.debug() << "DnsCache: Removing empty/outdated IP "
282 << "list for " << (*it).first.first;
283 IpCache.erase( (*it++).first );
292 cname_map_type::iterator it = CnameCache.begin();
293 cname_map_type::iterator it_end = CnameCache.end();
296 if ( (*it).second.Ttl.was_set_before( thresh ) )
298 GlobalLogger.debug() << "DnsCache: Removing outdated CNAME for "
300 CnameCache.erase( (*it++).first );
308 // -----------------------------------------------------------------------------
310 // -----------------------------------------------------------------------------
313 * warn if hostname is empty and remove trailing dot
314 * also warn if protocol is neither IPv4 nor IPv6
316 ip_map_key_type DnsCache::key_for_ips(const std::string &hostname,
317 const DnsIpProtocol &protocol) const
319 if (hostname.empty())
321 GlobalLogger.info() << "DnsCache: empty host!";
322 return ip_map_key_type("", DNS_IPALL);
324 if (protocol == DNS_IPALL)
326 GlobalLogger.info() << "DnsCache: neither IPv4 nor v6!";
327 return ip_map_key_type("", DNS_IPALL);
330 // check whether last character is a dot
331 if (hostname.rfind('.') == hostname.length()-1)
332 return ip_map_key_type( hostname.substr(0, hostname.length()-1),
335 return ip_map_key_type( hostname,
340 void DnsCache::update(const std::string &hostname,
341 const DnsIpProtocol &protocol,
342 const HostAddressVec &new_ips)
344 // check for valid input arguments
345 ip_map_key_type key = key_for_ips(hostname, protocol);
346 if ( key.first.empty() )
349 // ensure that there is never IP and CNAME for the same host
350 if ( !get_cname(hostname).Host.empty() )
352 GlobalLogger.info() << "DnsCache: Saving IPs for " << key.first
353 << " removes CNAME to " << get_cname(hostname).Host << "!";
354 update(hostname, Cname()); // overwrite with "empty" cname
357 // ensure min ttl of MinTimeBetweenResolves
358 HostAddressVec ips_checked;
359 BOOST_FOREACH( const HostAddress &addr, new_ips )
361 if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
363 GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
364 << key.first << " from " << addr.get_ttl().get_value() << "s to "
365 << MinTimeBetweenResolves << "s because was too short";
366 ips_checked.push_back( HostAddress( addr.get_ip(),
367 MinTimeBetweenResolves) );
370 ips_checked.push_back(addr);
373 // write IPs into one log line
374 stringstream log_temp;
375 log_temp << "DnsCache: update IPs for " << key.first << " to "
376 << ips_checked.size() << "-list: ";
377 BOOST_FOREACH( const HostAddress &ip, ips_checked )
378 log_temp << ip.get_ip() << ", ";
379 GlobalLogger.notice() << log_temp.str();
381 IpCache[key] = ips_checked;
387 * warn if hostname is empty and remove trailing dot
389 cname_map_key_type DnsCache::key_for_cname(const std::string &hostname) const
391 if (hostname.empty())
393 GlobalLogger.info() << "DnsCache: empty host!";
397 // check whether last character is a dot
398 if (hostname.rfind('.') == hostname.length()-1)
399 return hostname.substr(0, hostname.length()-1);
405 void DnsCache::update(const std::string &hostname,
408 // check for valid input arguments
409 cname_map_key_type key = key_for_cname(hostname);
413 // ensure that there is never IP and CNAME for the same host
414 int n_ips = get_ips(hostname, DNS_IPv4).size()
415 + get_ips(hostname, DNS_IPv6).size();
418 GlobalLogger.info() << "DnsCache: Saving IPs for " << key
419 << " removes CNAME to " << get_cname(hostname).Host << "!";
420 GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
421 << " removes " << n_ips << " IPs for same host!";
422 update(hostname, DNS_IPv4, HostAddressVec());
423 update(hostname, DNS_IPv6, HostAddressVec());
426 // remove possible trailing dot from cname's target host
427 Cname to_save = Cname(key_for_cname(cname.Host), // implicit cast to string
430 // ensure min ttl of MinTimeBetweenResolves
431 if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
433 GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
434 << key << " from " << to_save.Ttl.get_value() << "s to "
435 << MinTimeBetweenResolves << "s because was too short";
436 to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
439 GlobalLogger.notice() << "DnsCache: update CNAME for " << key
440 << " to " << to_save.Host;
441 CnameCache[key] = to_save;
446 // -----------------------------------------------------------------------------
448 // -----------------------------------------------------------------------------
451 * @returns empty list if no (up to date) ips for hostname in cache
453 HostAddressVec DnsCache::get_ips(const std::string &hostname,
454 const DnsIpProtocol &protocol,
455 const bool check_up_to_date)
457 ip_map_key_type key = key_for_ips(hostname, protocol);
458 HostAddressVec result = IpCache[key];
459 if (check_up_to_date)
461 HostAddressVec result_up_to_date;
462 uint32_t threshold = static_cast<uint32_t>(
463 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
464 uint32_t updated_ttl;
465 BOOST_FOREACH( const HostAddress &addr, result )
467 updated_ttl = addr.get_ttl().get_updated_value();
468 if (updated_ttl > threshold)
469 result_up_to_date.push_back(addr);
471 GlobalLogger.debug() << "DnsCache: do not return "
472 << addr.get_ip().to_string() << " since TTL "
473 << updated_ttl << "s is out of date (thresh="
474 << threshold << "s)";
476 result = result_up_to_date;
478 /*GlobalLogger.debug() << "DnsCache: request IPs for " << key.first
479 << " --> " << result.size() << "-list";
480 BOOST_FOREACH( const HostAddress &addr, result )
481 GlobalLogger.debug() << "DnsCache: " << addr.get_ip().to_string()
482 << " (TTL " << addr.get_ttl().get_updated_value()
488 * @returns empty cname if no (up to date cname) for hostname in cache
490 Cname DnsCache::get_cname(const std::string &hostname,
491 const bool check_up_to_date)
493 cname_map_key_type key = key_for_cname(hostname);
494 Cname result_obj = CnameCache[key];
495 /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
496 << " --> \"" << result_obj.Host << "\" (TTL "
497 << result_obj.Ttl.get_updated_value() << "s)";*/
498 if (result_obj.Host.empty())
501 else if (check_up_to_date)
503 if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
504 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
508 GlobalLogger.debug() << "DnsCache: CNAME is out of date";
509 return Cname(); // same as if there was no cname for hostname
516 // underlying assumption in this function: for a hostname, the cache has either
517 // a list of IPs saved or a cname saved, but never both
518 HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
519 const DnsIpProtocol &protocol,
520 const bool check_up_to_date)
522 std::string current_host = hostname;
524 HostAddressVec result = get_ips(current_host, protocol, check_up_to_date);
525 int n_recursions = 0;
526 uint32_t min_cname_ttl = 0xffff; // largest possible unsigned 4-byte value
527 int max_recursion_count = DnsMaster::get_instance()
528 ->get_max_recursion_count();
529 while ( result.empty() )
531 current_cname = get_cname(current_host, check_up_to_date);
532 if (current_cname.Host.empty())
533 break; // no ips (since result.empty()) and no cname
534 // --> will return empty result
536 current_host = current_cname.Host;
537 if (++n_recursions >= max_recursion_count)
539 GlobalLogger.info() << "DnsCache: reached recursion limit of "
540 << n_recursions << " in recursive IP retrieval of "
546 min_cname_ttl = min(min_cname_ttl,
547 current_cname.Ttl.get_updated_value());
548 result = get_ips(current_host, protocol, check_up_to_date);
552 GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
553 << result.size() << "-list after " << n_recursions
556 // adjust ttl to min of ttl and min_cname_ttl
557 if (n_recursions > 0)
559 TimeToLive cname_ttl(min_cname_ttl);
561 BOOST_FOREACH( HostAddress &addr, result )
563 if (addr.get_ttl().get_updated_value() > min_cname_ttl)
565 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
566 addr.set_ttl(cname_ttl);
575 * from a list of CNAMEs find the first one that is out of date or empty
577 * returns the hostname that is out of date or empty if all CNAMEs are
580 * required in ResolverBase::get_skipper
582 std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
583 const uint32_t ttl_thresh)
585 std::string first_outdated = hostname;
587 int n_recursions = 0;
588 int max_recursion_count = DnsMaster::get_instance()
589 ->get_max_recursion_count();
592 if (++n_recursions >= max_recursion_count)
594 GlobalLogger.info() << "DnsCache: reached recursion limit of "
595 << n_recursions << " in search of outdated CNAMEs for "
597 return first_outdated; // not really out of date but currently
600 cname = get_cname(first_outdated);
601 if (cname.Host.empty())
602 // reached end of cname list --> everything was up-to-date
604 else if (cname.Ttl.get_updated_value() > ttl_thresh)
605 // cname is up to date --> continue looking
606 first_outdated = cname.Host;
608 // cname is out of date --> return its target
611 // reach this point only if cname chain does not end with an IP
612 // --> all are up-to-date
616 std::string DnsCache::get_cname_chain_str(const std::string &hostname)
618 std::stringstream temp;
620 std::string current_host = hostname;
622 int n_recursions = 0;
623 int max_recursion_count = DnsMaster::get_instance()
624 ->get_max_recursion_count();
627 if (n_recursions >= max_recursion_count)
633 current_cname = get_cname(current_host, false);
634 if (current_cname.Host.empty())
638 current_host = current_cname.Host;
639 temp << "-->" << current_host;
647 // -----------------------------------------------------------------------------
649 // -----------------------------------------------------------------------------
650 void DnsCache::debug_print() const
652 GlobalLogger.debug() << "DnsCache: IP Cache contents:";
653 stringstream log_temp;
654 BOOST_FOREACH( const ip_map_type::value_type &key_and_ip, IpCache )
656 // write IPs into one log line
658 log_temp << "DnsCache: " << key_and_ip.first.first << ": \t "
659 << key_and_ip.second.size() << "-list ";
660 BOOST_FOREACH( const HostAddress &ip, key_and_ip.second )
661 log_temp << ip.get_ip() << "+" << ip.get_ttl().get_updated_value()
663 GlobalLogger.debug() << log_temp.str();
666 GlobalLogger.debug() << "DnsCache: CNAME Cache contents:";
667 BOOST_FOREACH( const cname_map_type::value_type &key_and_cname, CnameCache )
668 GlobalLogger.debug() << "DnsCache: " << key_and_cname.first << ": \t "
669 << key_and_cname.second.Host << "+"
670 << key_and_cname.second.Ttl.get_updated_value()