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
22 with code copied from boost::net::dns::resolve.hpp
23 by Andreas Haberstroh (andreas at ibusy dot com)
24 from https://github.com/softwareace/Boost.DNS
27 #include "dns/dnsresolver.h"
32 #include <boost/foreach.hpp>
33 #include <boost/bind.hpp>
34 #include <boost/function.hpp>
35 #include <boost/net/dns.hpp>
36 #include <boost/date_time/posix_time/posix_time.hpp>
37 #include <boost/uuid/uuid.hpp>
38 #include <boost/uuid/uuid_io.hpp>
40 #include <logfunc.hpp>
42 using I2n::Logger::GlobalLogger;
43 using boost::posix_time::seconds;
47 const int ResolveTimeoutSeconds = 2;
48 const int PauseBeforeRetrySeconds = 1;
49 const int StaleDataLongtermSeconds = 5*60;
50 const int DNS_PORT = 53;
53 DnsResolver::DnsResolver(IoServiceItem &io_serv,
54 const std::string &hostname,
55 const DnsIpProtocol &protocol,
56 const DnsCacheItem cache,
57 const boost::asio::ip::address &name_server)
58 : ResolverBase( io_serv, hostname, cache )
59 , Socket( *io_serv, ip::udp::endpoint(ip::udp::v4(), 0))
60 // just connect to anything, will specify sender/receiver later
63 , Protocol( protocol )
64 , NameServer( name_server, Config::DNS_PORT )
65 , ResolveTimeoutTimer( *io_serv )
66 , PauseBeforeRetryTimer( *io_serv )
67 , StaleDataLongtermTimer( *io_serv )
70 , IsResolving( false )
71 , LogPrefix( "DnsResolver" )
74 , OperationCancelled( false )
75 , LongtermTimerIsActive( false )
77 std::stringstream temp;
78 temp << "Dns(" << ResolverBase::Hostname << "): ";
79 LogPrefix = temp.str();
83 DnsResolver::~DnsResolver()
85 boost::system::error_code error;
86 //Socket.shutdown(boost::asio::ip::udp::socket::shutdown_both, error);
88 // GlobalLogger.warning() << LogPrefix << "Received error " << error
89 // << " when shutting down socket for DNS";
90 // in IcmpPinger always gave an error system:9 (EBADF: Bad file descriptor)
91 // Here gives error system:107 ENOTCONN: Transport endpoint is not connected
95 GlobalLogger.warning() << LogPrefix << "Received error " << error
96 << " when closing socket for DNS";
101 //==============================================================================
103 //==============================================================================
106 * copied here code from boost::net::dns::resolve.hpp, since want async
107 * operation and that is used only internally, there
109 void DnsResolver::do_resolve()
111 // check if resolving already
114 GlobalLogger.info() << LogPrefix
115 << "Call to do_resolve ignored since resolving already";
119 OperationCancelled = false;
121 GlobalLogger.info() << LogPrefix << "start resolving for IPs of type "
122 << to_string(Protocol) << " using name server " << NameServer;
124 // just to be sure: cancel timers
125 ResolveTimeoutTimer.cancel();
126 PauseBeforeRetryTimer.cancel();
127 StaleDataLongtermTimer.cancel();
128 LongtermTimerIsActive = false;
130 // create DNS request
131 boost::net::dns::message dns_message( ResolverBase::Hostname, Protocol );
132 dns_message.recursive(true);
133 dns_message.action(boost::net::dns::message::query);
134 dns_message.opcode(boost::net::dns::message::squery);
136 // create random ID for message
137 boost::uuids::uuid message_id = RandomIdGenerator();
138 memcpy( &RequestId, message_id.data, sizeof(RequestId) );
139 dns_message.id( RequestId );
140 GlobalLogger.debug() << LogPrefix << "Request has ID "
141 << std::showbase << std::hex << dns_message.id();
143 // setup receipt of reply
144 Socket.async_receive_from(
145 boost::asio::buffer(ReceiveBuffer.get_array()),
147 boost::bind( &DnsResolver::handle_dns_result, this,
148 boost::asio::placeholders::error,
149 boost::asio::placeholders::bytes_transferred)
153 (void) ResolveTimeoutTimer.expires_from_now(
154 seconds(Config::ResolveTimeoutSeconds));
155 ResolveTimeoutTimer.async_wait( boost::bind(
156 &DnsResolver::handle_resolve_timeout,
157 this, boost::asio::placeholders::error) );
160 dns_message.encode(RequestBuffer);
164 bytes_sent = Socket.send_to(
165 boost::asio::buffer(RequestBuffer.get_array()),
168 catch (boost::system::system_error &err)
170 GlobalLogger.warning() << LogPrefix
171 << "Sending of DNS request message failed: "
177 if ( bytes_sent == 0 )
179 GlobalLogger.warning() << LogPrefix << "Empty DNS request sent!";
186 void DnsResolver::handle_dns_result(const boost::system::error_code &error,
187 const std::size_t bytes_transferred)
191 GlobalLogger.info() << LogPrefix << "DNS resolve resulted in error "
192 << error << " --> request retry";
196 else if ( OperationCancelled )
197 { // async_resolve was cancelled --> callbacks already called
198 GlobalLogger.info() << LogPrefix
199 << "Ignoring DNS results since we were cancelled";
203 GlobalLogger.debug() << LogPrefix << "Handling DNS result ("
204 << bytes_transferred << " bytes transferred)";
206 // next 3(+1) lines copied from boost/net/dns/resolver.hpp:
207 // clamp the recvBuffer with the number of bytes transferred or decode buffr
208 ReceiveBuffer.length(bytes_transferred);
209 boost::net::dns::message result_message;
210 result_message.decode( ReceiveBuffer );
213 if (RequestId != result_message.id())
214 GlobalLogger.warning() << LogPrefix << "Received answer for request ID "
215 << std::showbase << std::hex << result_message.id()
216 << " but expected ID " << RequestId;
218 GlobalLogger.debug() << LogPrefix << "Result has correct ID "
219 << std::showbase << std::hex << RequestId;
222 // loop over answers, remembering ips and cnames
223 // work with a regular pointer to list of answers since result_message is
224 // owner of data and that exists until end of function
225 // Items in answers list are shared_ptr to resource_base_t
226 std::vector<host_addr_pair> result_ips;
227 std::vector<src_cname_pair> result_cnames;
228 std::vector<string_pair> result_name_servers;
230 GlobalLogger.debug() << LogPrefix <<"Checking ANSWERS section of dns reply";
231 gather_results(result_message.answers(), &result_ips, &result_cnames,
232 &result_name_servers);
233 // results should have the logical order
234 // Hostname [ --> cname1 --> cname2 --> ... --> cnameN ] [ --> ips ]
236 // remember cname list (if there were any)
237 BOOST_FOREACH( const src_cname_pair &host_and_cname, result_cnames )
238 ResolverBase::update_cache(host_and_cname.first, host_and_cname.second);
240 if ( !result_ips.empty() )
241 handle_ips( result_ips );
242 else if ( !result_cnames.empty() )
243 // no IPs but at least one cname --> find the "last" cname and
244 // re-start resolving with that
245 handle_cname(result_cnames);
247 { // no answers --> cannot proceed
248 GlobalLogger.warning() << LogPrefix << "No IP nor CNAME received! "
249 << "--> request retry";
255 * gather IPs, CNAMEs and name servers from list of resource records;
257 * can be run on anwers(), autorities() and additional() sections of dns reply
260 * @param rr_list: input list of resource records
261 * @param result_ips: output vector of ips
262 * @param result_cnames: output vector of cnames
263 * @param result_name_servers: output vector of name servers
265 void DnsResolver::gather_results(const boost::net::dns::rr_list_t *rr_list,
266 std::vector<host_addr_pair> *result_ips,
267 std::vector<src_cname_pair> *result_cnames,
268 std::vector<string_pair> *result_name_servers)
271 using boost::net::dns::resource_base_t;
272 boost::posix_time::ptime now =boost::posix_time::second_clock::local_time();
273 BOOST_FOREACH( boost::shared_ptr<resource_base_t> rr_item, *rr_list )
275 boost::net::dns::type_t rr_type = rr_item->rtype();
276 uint32_t ttl = rr_item->ttl();
277 std::string domain = rr_item->domain();
279 boost::posix_time::to_simple_string(now + seconds(ttl));
281 if (rr_type == boost::net::dns::type_a)
282 { // 'A' resource records carry IPv4 addresses
283 if (Protocol == DNS_IPv6)
285 GlobalLogger.info() << LogPrefix << "Ignoring IPv4 address "
286 << "because resolver was configured to only use IPv6.";
289 boost::asio::ip::address_v4 ip =
290 ( dynamic_cast<boost::net::dns::a_resource *> (rr_item.get()) )
292 result_ips->push_back(host_addr_pair(domain, HostAddress(ip, ttl)));
293 GlobalLogger.debug() << LogPrefix << domain << ": IPv4 " << ip
294 << " with TTL " << ttl << "s (until "
297 else if (rr_type == boost::net::dns::type_a6)
298 { // 'AAAA' resource records carry IPv6 addresses
299 if (Protocol == DNS_IPv4)
301 GlobalLogger.info() << LogPrefix << "Ignoring IPv6 address "
302 << "because resolver was configured to only use IPv4.";
305 boost::asio::ip::address_v6 ip =
306 ( dynamic_cast<boost::net::dns::a6_resource *> (rr_item.get()) )
308 result_ips->push_back(host_addr_pair(domain, HostAddress(ip, ttl)));
309 GlobalLogger.debug() << LogPrefix << domain << ": IPv6 " << ip
310 << " with TTL " << ttl << "s (until "
313 else if (rr_type == boost::net::dns::type_cname)
314 { // 'CNAME' resource records that carry aliases
316 (dynamic_cast<boost::net::dns::cname_resource *>(rr_item.get()))
318 result_cnames->push_back( src_cname_pair(domain,
319 Cname(cname, ttl)) );
320 GlobalLogger.debug() << LogPrefix << domain << ": CNAME to "
321 << cname << " with TTL " << ttl << "s (until "
324 else if (rr_type == boost::net::dns::type_ns)
325 { // NS (name_server) resource records
326 std::string name_server =
327 (dynamic_cast<boost::net::dns::ns_resource *>(rr_item.get()))
329 result_name_servers->push_back( string_pair(domain, name_server) );
330 GlobalLogger.debug() << LogPrefix << "NameServer " << name_server
331 << " for " << domain << " with TTL " << ttl
332 << "s (until " << expiry << ")";
334 else if (rr_type == boost::net::dns::type_soa)
335 GlobalLogger.debug() << LogPrefix << "SOA resource";
336 else if (rr_type == boost::net::dns::type_ptr)
337 GlobalLogger.debug() << LogPrefix << "ptr resource";
338 else if (rr_type == boost::net::dns::type_hinfo)
339 GlobalLogger.debug() << LogPrefix << "hinfo resource";
340 else if (rr_type == boost::net::dns::type_mx)
341 GlobalLogger.debug() << LogPrefix << "mx resource";
342 else if (rr_type == boost::net::dns::type_txt)
343 GlobalLogger.debug() << LogPrefix << "txt resource";
344 else if (rr_type == boost::net::dns::type_srv)
345 GlobalLogger.debug() << LogPrefix << "srv resource";
346 else if (rr_type == boost::net::dns::type_axfr)
347 GlobalLogger.debug() << LogPrefix << "axfr resource";
349 GlobalLogger.debug() << LogPrefix << "unknown resource type: "
350 << std::showbase << std::hex
351 << static_cast<unsigned>(rr_item->rtype());
356 void DnsResolver::handle_unavailable()
358 // schedule new attempt in quite a while
359 StaleDataLongtermTimer.expires_from_now(
360 seconds(Config::StaleDataLongtermSeconds));
361 StaleDataLongtermTimer.async_wait(
362 boost::bind( &DnsResolver::wait_timer_timeout_handler,
363 this, boost::asio::placeholders::error
366 LongtermTimerIsActive = true;
368 // for now, admit failure
369 bool was_success = false;
370 finalize_resolve(was_success);
374 void DnsResolver::handle_ips(const std::vector<host_addr_pair> &result_ips)
376 // received at least one IP which could be for the queried host name
377 // or the cname at the "end" of the cname list;
378 // but all IPs should be for the same
379 HostAddressVec addr_list;
380 std::string only_host_for_ips = result_ips[0].first;
381 BOOST_FOREACH( const host_addr_pair &host_and_addr, result_ips)
383 if ( host_and_addr.first != only_host_for_ips )
384 GlobalLogger.warning() << LogPrefix
385 << "Received IPs for different hosts " << only_host_for_ips
386 << " and " << host_and_addr.first << " in one DNS result! "
387 << "--> ignore second";
390 GlobalLogger.notice() << LogPrefix << "Found IP "
391 << host_and_addr.second.get_ip() << " with TTL "
392 << host_and_addr.second.get_ttl().get_value() << "s";
393 addr_list.push_back(host_and_addr.second);
396 ResolverBase::update_cache( only_host_for_ips, addr_list );
399 bool was_success = true;
400 finalize_resolve(was_success);
404 void DnsResolver::handle_cname(const std::vector<src_cname_pair> &result_cnames)
406 // find the "last" cname in the list
407 // Hostname --> cname1 --> cname2 --> ... --> cnameN
408 // We assume here that this list might not be in order but that all cnames
409 // form a single list (form one connected list and not several isolated)
411 std::string last_cname = "";
413 BOOST_REVERSE_FOREACH( const src_cname_pair &host_and_cname, result_cnames )
415 could_be_last = true;
416 BOOST_REVERSE_FOREACH( const src_cname_pair &other, result_cnames )
418 if (other.first == host_and_cname.second.Host)
419 { // found cname for current cname
420 could_be_last = false;
426 last_cname = host_and_cname.second.Host;
431 if (last_cname.empty())
433 GlobalLogger.error() << LogPrefix
434 << "Could not identify \"last\" CNAME to handle -- "
435 << "maybe we encountered a CNAME loop? Anyway, cannot proceed!";
436 GlobalLogger.info() << LogPrefix << "Result CNAMEs were:";
437 BOOST_FOREACH( const src_cname_pair &host_and_cname, result_cnames )
438 GlobalLogger.info() << LogPrefix << host_and_cname.first << " --> "
439 << host_and_cname.second.Host;
440 handle_unavailable();
443 { // check cache for IP for this cname
444 bool check_up_to_date = true;
445 HostAddressVec cached_data = Cache->get_ips_recursive(last_cname,
447 if ( !cached_data.empty() )
449 bool was_success = true;
450 int cname_count = 1; // define cache access as only 1
451 finalize_resolve(was_success, cname_count);
454 { // get resolver for canonical name
455 ResolverItem resolver = DnsMaster::get_instance()
456 ->get_resolver_for(last_cname, Protocol);
457 callback_type callback = boost::bind(
458 &DnsResolver::cname_resolve_callback,
460 resolver->async_resolve( callback );
462 // treat a CNAME as a partial result: not enough to run callbacks
463 // from finalize_resolve, but enough to stop timers and reset
464 // RetryCount --> name resolution can take longer
471 void DnsResolver::cname_resolve_callback(const bool was_success,
472 const int cname_count)
474 if ( OperationCancelled )
475 { // async_resolve was cancelled --> callbacks already called
476 GlobalLogger.info() << LogPrefix
477 << "Ignoring CNAME results since we were cancelled";
480 else if (was_success)
482 GlobalLogger.debug() << LogPrefix << "CNAME resolution succeeded";
483 finalize_resolve(was_success, cname_count+1);
487 GlobalLogger.info() << LogPrefix << "CNAME resolution failed";
488 // no use to schedule retry in this case since cname resolver must have
489 // failed several times and we can only re-start the same procedure with
490 // the same information. But can re-try later
491 handle_unavailable();
496 void DnsResolver::finalize_resolve(const bool was_success,
497 const int cname_count)
499 // some consistency checks; failure might indicate a situation I had not
500 // anticipated during programming but might not be harmfull yet
502 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
503 << "not resolving any more!";
504 if ( OperationCancelled )
505 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
506 << " was cancelled!";
507 if ( ResolverBase::CallbackList.empty() )
508 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
510 if ( RequestId != 0 )
511 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
512 << "waiting for DNS reply!";
515 stop_trying(was_success);
517 // schedule callbacks, clearing callback list
518 ResolverBase::schedule_callbacks(was_success, cname_count);
521 GlobalLogger.notice() << LogPrefix << "finalized resolve"
522 << " with success = " << was_success
523 << " and cname_count = " << cname_count;
529 * arg was_success determines if stop trying forever or just for the moment
530 * --> determines if we cancel StaleDataLongtermTimer or not
532 void DnsResolver::stop_trying(bool was_success)
535 GlobalLogger.debug() << LogPrefix << "Cancelling timers";
536 ResolveTimeoutTimer.cancel();
537 PauseBeforeRetryTimer.cancel();
541 StaleDataLongtermTimer.cancel();
542 LongtermTimerIsActive = false;
551 * return true if resolver is currently resolving
553 * Is true from call to async_resolve until callbacks
554 * --> returns true if waiting for result or (short-term) retry
556 * However, does NOT tell you if the (long-term) stale timeout is active!
557 * That timer has no effect on result, need to check is_waiting_to_resolve
560 bool DnsResolver::is_resolving() const
567 * returns true if either is_resolving or the long-term timer is active
569 * is_resolving returns true if the short-term retry timer is active
571 bool DnsResolver::is_waiting_to_resolve() const
573 return IsResolving || LongtermTimerIsActive;
578 * cancel a earlier call to async_resolve
580 * callbacks will be called with was_success=false; all internal operations
581 * will be cancelled and internal callbacks (timers, dns results) have no
582 * effect any more; cancels also the long-term stale-data timer
584 void DnsResolver::cancel_resolve()
586 if ( !IsResolving && !LongtermTimerIsActive)
588 GlobalLogger.info() << LogPrefix << "Cancel called on non-resolving, "
589 << "non-waiting resolver -- ignore";
592 else if (OperationCancelled)
594 GlobalLogger.info() << LogPrefix
595 << "Cancel called on cancelled resolver -- ignore";
598 GlobalLogger.info() << LogPrefix << "Cancel resolver";
600 // set before finalize_resolve so can check in finalize_resolve that ID is
601 // always 0; ID is not used any more since handle_dns_result stops if
602 // OperationCancelled is true
607 bool was_success = false;
609 finalize_resolve(was_success, cname_count);
612 // also cancel the long-term timer
613 StaleDataLongtermTimer.cancel();
614 LongtermTimerIsActive = false;
616 // set after finalize_resolve, so can check in finalize_resolve that
617 // OperationCancelled is never true
618 OperationCancelled = true;
623 void DnsResolver::handle_resolve_timeout(const boost::system::error_code &error)
625 if ( error == boost::asio::error::operation_aborted ) // cancelled
627 GlobalLogger.debug() << LogPrefix
628 << "Resolve timeout timer was cancelled!";
633 GlobalLogger.warning() << LogPrefix
634 << "resolve timeout handler received error "
635 << error << " --> request retry";
638 else if ( OperationCancelled )
639 { // async_resolve was cancelled --> callbacks already called
640 GlobalLogger.info() << LogPrefix
641 << "Ignoring DNS timeout since we were cancelled";
646 GlobalLogger.notice() << LogPrefix << "DNS resolving timed out";
652 void DnsResolver::schedule_retry()
655 ResolveTimeoutTimer.cancel();
656 PauseBeforeRetryTimer.cancel();
661 if ( RetryCount > DnsMaster::get_instance()
662 ->get_max_address_resolution_attempts() )
663 { // too many re-tries
664 GlobalLogger.info() << LogPrefix << "Not scheduling a retry since "
665 << "RetryCount " << RetryCount << " too high";
666 handle_unavailable(); // will call stop_trying i.e. reset RetryCount
670 GlobalLogger.info() << LogPrefix << "Scheduling a retry (RetryCount="
671 << RetryCount << ")";
672 PauseBeforeRetryTimer.expires_from_now(
673 seconds(Config::PauseBeforeRetrySeconds));
674 PauseBeforeRetryTimer.async_wait(
675 boost::bind( &DnsResolver::wait_timer_timeout_handler,
676 this, boost::asio::placeholders::error) );
680 void DnsResolver::wait_timer_timeout_handler(
681 const boost::system::error_code &error)
683 if ( error == boost::asio::error::operation_aborted ) // cancelled
684 { // assume that our code cancelled this timer, so callbacks will be
686 GlobalLogger.debug() << LogPrefix
687 << "Resolve wait timer was cancelled! ";
690 { // not sure what to do here, but callers waiting forever for a callback
691 // is probably the worst thing to happen, so call finalize_resolve
692 GlobalLogger.warning() << LogPrefix
693 << "resolve wait handler received error "
694 << error << "! Try to finalize resolve";
695 bool was_success = false;
696 finalize_resolve(was_success);
698 else if ( OperationCancelled )
699 { // async_resolve was cancelled --> callbacks already called
700 GlobalLogger.info() << LogPrefix
701 << "Ignoring waiting timeout since we were cancelled";
706 GlobalLogger.info() << LogPrefix << "Done waiting --> re-try resolve";
707 IsResolving = false; // will be set to true immediately in do_resolve
713 //==============================================================================
715 //==============================================================================
717 HostAddress DnsResolver::get_next_ip(bool check_up_to_date)
720 // (do not use arg check_up_to_date here in order to give NextIpIndex
721 // a chance to stay above number of outdated IPs)
722 HostAddressVec cached_data = ResolverBase::get_cached_ips_recursively();
724 // if no results cached, return default-constructed HostAddress (0.0.0.0)
725 HostAddress return_candidate;
726 if ( cached_data.empty() )
728 GlobalLogger.debug() << LogPrefix << "Get next IP: nothing cached";
729 return return_candidate;
732 std::size_t n_iter = 0;
733 std::size_t n_ips = cached_data.size();
734 uint32_t ttl_thresh = static_cast<uint32_t>( DnsMaster::get_instance()
735 ->get_resolved_ip_ttl_threshold() );
737 GlobalLogger.info() << LogPrefix << "Get next IP from cached result of "
738 << n_ips << " IPs; first index to consider is " << NextIpIndex
739 << "; TTL thresh=" << ttl_thresh << "s is used: " << check_up_to_date;
741 // loop until we have found a cached result (that is up to date)
742 // or until we have tried all cached IPs
745 // check index since cache size may have changed since last call
746 if (NextIpIndex >= n_ips)
748 GlobalLogger.debug() << LogPrefix << "Reset NextIpIndex";
751 else if ( n_iter >= n_ips)
753 GlobalLogger.debug() << LogPrefix << "No IP found";
754 return HostAddress(); // have checked all candidates
757 { // there are candidates left to consider
758 GlobalLogger.debug() << LogPrefix << "Check IP candidate at index "
760 return_candidate = cached_data[NextIpIndex++];
761 if (!check_up_to_date)
763 GlobalLogger.debug() << LogPrefix << "not checking ttl, accept";
764 return return_candidate;
766 else if (return_candidate.get_ttl().get_updated_value()
769 GlobalLogger.debug() << LogPrefix << "is up to date, accept";
770 return return_candidate;
774 GlobalLogger.debug() << LogPrefix << "is out of date ("
775 << return_candidate.get_ttl().get_updated_value()
776 << "s <= " << ttl_thresh << "s), continue";
783 bool DnsResolver::have_up_to_date_ip()
785 return get_resolved_ip_count(true) > 0;
788 int DnsResolver::get_resolved_ip_count(const bool check_up_to_date)
790 // run with empty hostname --> uses internal var Hostname
791 return ResolverBase::get_cached_ips_recursively("",check_up_to_date).size();