fixed bug that caused outdated IPsto be returned from cache; added test for that
[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
946356e1 25#include <fstream>
96779587 26#include <logfunc.hpp>
946356e1 27#include <filefunc.hxx> // I2n::file_exists
e18c1337 28#include <boost/foreach.hpp>
96779587
CH
29#include <boost/bind.hpp>
30#include <boost/date_time/posix_time/posix_time.hpp>
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{
48 int SaveTimerSeconds = 60;
dbe986b9 49 int MaxRetrievalRecursions = 10;
96779587
CH
50}
51
23f51766
CH
52Cname::Cname()
53 : Host()
54 , Ttl()
55{}
56
57Cname::Cname(const std::string &host, const uint32_t ttl)
58 : Host( host )
59 , Ttl( ttl )
60{}
61
26b0f687
CH
62Cname::Cname(const std::string &host, const TimeToLive &ttl)
63 : Host( host )
64 , Ttl( ttl )
65{}
66
8d26221d
CH
67
68const string DnsCache::DoNotUseCacheFile = "do not use cache file!";
69
96779587
CH
70DnsCache::DnsCache(const IoServiceItem &io_serv,
71 const std::string &cache_file)
ad83004d
CH
72 : IpCache()
73 , CnameCache()
96779587
CH
74 , SaveTimer( *io_serv )
75 , CacheFile( cache_file )
76 , HasChanged( false )
77{
78 // load cache from file
79 load_from_cachefile();
80
81 // schedule next save
82 (void) SaveTimer.expires_from_now( seconds( Config::SaveTimerSeconds ) );
83 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
84 boost::asio::placeholders::error ) );
85}
86
87
88DnsCache::~DnsCache()
89{
26b0f687 90 GlobalLogger.info() << "DnsCache: being destructed";
e91538f0 91
96779587
CH
92 // save one last time without re-scheduling the next save
93 save_to_cachefile();
94
95 // cancel save timer
96 SaveTimer.cancel();
97}
98
99
100void DnsCache::schedule_save(const boost::system::error_code &error)
101{
102 // just in case: ensure SaveTimer is cancelled
103 SaveTimer.cancel(); // (will do nothing if already expired/cancelled)
104
105 if ( error == boost::asio::error::operation_aborted ) // cancelled
106 {
ad83004d 107 GlobalLogger.error() << "DnsCache: SaveTimer was cancelled "
96779587
CH
108 << "--> no save and no re-schedule of saving!";
109 return;
110 }
111 else if (error)
112 {
ad83004d 113 GlobalLogger.error() << "DnsCache: Received error " << error
96779587
CH
114 << " in schedule_save "
115 << "--> no save now but re-schedule saving";
116 }
117 else
118 save_to_cachefile();
119
120 // schedule next save
121 (void) SaveTimer.expires_from_now( seconds( Config::SaveTimerSeconds ) );
122 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
123 boost::asio::placeholders::error ) );
124}
125
126void DnsCache::save_to_cachefile()
127{
128 if (!HasChanged)
ad83004d 129 GlobalLogger.info() << "DnsCache: skip saving because has not changed";
946356e1 130 else if (CacheFile.empty())
946356e1 131 GlobalLogger.warning()
ad83004d 132 << "DnsCache: skip saving because file name empty!";
8d26221d 133 else if (CacheFile == DoNotUseCacheFile)
26b0f687
CH
134 GlobalLogger.info() << "DnsCache: configured not to use cache file";
135 else
946356e1 136 {
26b0f687
CH
137 try
138 {
139 std::ofstream ofs( CacheFile.c_str() );
140 boost::archive::xml_oarchive oa(ofs);
141 //oa << boost::serialization::make_nvp("IpCache", IpCache);
142 //oa << boost::serialization::make_nvp("CnameCache", CnameCache);
143 oa & BOOST_SERIALIZATION_NVP(IpCache);
144 oa & BOOST_SERIALIZATION_NVP(CnameCache);
8d26221d
CH
145 GlobalLogger.info() << "DnsCache: saved to cache file "
146 << CacheFile;
26b0f687
CH
147
148 HasChanged = false;
149 }
150 catch (std::exception &exc)
151 {
c1abff61 152 GlobalLogger.warning() << "DnsCache: Saving failed: " << exc.what();
26b0f687 153 }
946356e1 154 }
96779587
CH
155}
156
96779587
CH
157void DnsCache::load_from_cachefile()
158{
946356e1 159 if (CacheFile.empty())
946356e1 160 GlobalLogger.warning()
8d26221d
CH
161 << "DnsCache: cannot load because cache file name is empty!";
162 else if (CacheFile == DoNotUseCacheFile)
26b0f687 163 GlobalLogger.info() << "DnsCache: configured not to use cache file";
946356e1 164 else if ( !I2n::file_exists(CacheFile) )
ad83004d 165 GlobalLogger.warning() << "DnsCache: cannot load because cache file "
946356e1 166 << CacheFile << " does not exist!";
26b0f687 167 else
946356e1 168 {
26b0f687
CH
169 try
170 {
171 std::ifstream ifs( CacheFile.c_str() );
172 boost::archive::xml_iarchive ia(ifs);
946356e1 173
26b0f687
CH
174 ia & BOOST_SERIALIZATION_NVP(IpCache);
175 ia & BOOST_SERIALIZATION_NVP(CnameCache);
176 GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
177 }
178 catch (boost::archive::archive_exception &exc)
179 {
8d26221d
CH
180 GlobalLogger.warning()
181 << "DnsCache: archive exception loading from " << CacheFile
182 << ": " << exc.what();
26b0f687
CH
183 }
184 catch (std::exception &exc)
185 {
186 GlobalLogger.warning() << "DnsCache: exception while loading from "
187 << CacheFile << ": " << exc.what();
188 }
946356e1 189 }
96779587
CH
190}
191
26b0f687
CH
192
193// warn if hostname is empty and remove trailing dot
194std::string DnsCache::key_for_hostname(const std::string &hostname) const
96779587 195{
26b0f687
CH
196 if (hostname.empty())
197 {
198 GlobalLogger.warning() << "DnsCache: empty host!";
199 return "";
200 }
201
202 // check whether last character is a dot
203 if (hostname.rfind('.') == hostname.length()-1)
204 return hostname.substr(0, hostname.length()-1);
205 else
206 return hostname;
96779587
CH
207}
208
209
ad83004d 210void DnsCache::update(const std::string &hostname,
26b0f687 211 const HostAddressVec &new_ips)
ad83004d 212{
26b0f687
CH
213 std::string key = key_for_hostname(hostname);
214 if ( !get_cname(hostname).Host.empty() )
215 { // ensure that there is never IP and CNAME for the same host
216 GlobalLogger.warning() << "DnsCache: Saving IPs for " << key
217 << " removes CNAME to " << get_cname(hostname).Host << "!";
218 update(hostname, Cname()); // overwrite with "empty" cname
219 }
220 GlobalLogger.debug() << "DnsCache: update IPs for " << key
221 << " to " << new_ips.size() << "-list";
222 IpCache[key] = new_ips;
ad83004d
CH
223 HasChanged = true;
224}
225
226
dbe986b9 227void DnsCache::update(const std::string &hostname,
26b0f687 228 const Cname &cname)
ad83004d 229{
26b0f687
CH
230 std::string key = key_for_hostname(hostname);
231 if ( !get_ips(hostname).empty() )
232 { // ensure that there is never IP and CNAME for the same host
233 GlobalLogger.warning() << "DnsCache: Saving CNAME for " << key
234 << " removes " << get_ips(hostname).size() << " IPs for same host!";
235 update(hostname, HostAddressVec()); // overwrite with empty IP list
ad83004d 236 }
26b0f687
CH
237
238 // remove possible trailing dot from cname
239 Cname to_save = Cname(key_for_hostname(cname.Host),
240 cname.Ttl);
241
242 GlobalLogger.info() << "DnsCache: update CNAME for " << key
243 << " to " << to_save.Host;
244 CnameCache[key] = to_save;
245 HasChanged = true;
ad83004d
CH
246}
247
248
23f51766
CH
249/**
250 * @returns empty list if no (up to date) ips for hostname in cache
251 */
dbe986b9
CH
252HostAddressVec DnsCache::get_ips(const std::string &hostname,
253 const bool check_up_to_date)
96779587 254{
26b0f687
CH
255 std::string key = key_for_hostname(hostname);
256 HostAddressVec result = IpCache[key];
dbe986b9
CH
257 if (check_up_to_date)
258 {
259 HostAddressVec result_up_to_date;
23f51766
CH
260 uint32_t threshold = static_cast<uint32_t>(
261 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
26b0f687 262 uint32_t updated_ttl;
dbe986b9
CH
263 BOOST_FOREACH( const HostAddress &addr, result )
264 {
26b0f687
CH
265 updated_ttl = addr.get_ttl().get_updated_value();
266 if (updated_ttl > threshold)
dbe986b9 267 result_up_to_date.push_back(addr);
26b0f687
CH
268 else
269 GlobalLogger.debug() << "DnsCache: do not return "
270 << addr.get_ip().to_string() << " since TTL "
271 << updated_ttl << "s is out of date (thresh="
272 << threshold << "s)";
dbe986b9 273 }
dbe986b9
CH
274 result = result_up_to_date;
275 }
26b0f687
CH
276 GlobalLogger.debug() << "DnsCache: request IPs for " << key
277 << " --> " << result.size() << "-list";
278 BOOST_FOREACH( const HostAddress &addr, result )
279 GlobalLogger.debug() << "DnsCache: " << addr.get_ip().to_string()
fd62d09f
CH
280 << " (TTL " << addr.get_ttl().get_updated_value()
281 << "s)";
dbe986b9 282 return result;
96779587
CH
283}
284
23f51766
CH
285/**
286 * @returns empty cname if no (up to date cname) for hostname in cache
287 */
288Cname DnsCache::get_cname(const std::string &hostname,
289 const bool check_up_to_date)
ad83004d 290{
26b0f687
CH
291 std::string key = key_for_hostname(hostname);
292 Cname result_obj = CnameCache[key];
293 GlobalLogger.debug() << "DnsCache: request CNAME for " << key
fd62d09f
CH
294 << " --> \"" << result_obj.Host << "\" (TTL "
295 << result_obj.Ttl.get_updated_value() << "s)";
23f51766
CH
296 if (result_obj.Host.empty())
297 return result_obj;
298
299 else if (check_up_to_date)
dbe986b9 300 {
26b0f687
CH
301 if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
302 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
23f51766 303 return result_obj;
dbe986b9
CH
304 else
305 {
23f51766
CH
306 GlobalLogger.debug() << "DnsCache: CNAME is out of date";
307 return Cname(); // same as if there was no cname for hostname
dbe986b9
CH
308 }
309 }
310 else
23f51766 311 return result_obj;
ad83004d
CH
312}
313
dbe986b9
CH
314// underlying assumption in this function: for a hostname, the cache has either
315// a list of IPs saved or a cname saved, but never both
316HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
317 const bool check_up_to_date)
e18c1337
CH
318{
319 std::string current_host = hostname;
23f51766 320 Cname current_cname;
00c81aa0 321 HostAddressVec result = get_ips(current_host, check_up_to_date);
dbe986b9 322 int n_recursions = 0;
23f51766 323 uint32_t min_cname_ttl = 0xffff; // largest possible unsigned 4-byte value
e18c1337
CH
324 while ( result.empty() )
325 {
23f51766 326 current_cname = get_cname(current_host, check_up_to_date);
8d26221d 327 if (current_cname.Host.empty())
e18c1337 328 break;
8d26221d
CH
329
330 current_host = key_for_hostname(current_cname.Host);
331 if (++n_recursions >= Config::MaxRetrievalRecursions)
dbe986b9
CH
332 {
333 GlobalLogger.warning() << "DnsCache: reached recursion limit of "
334 << n_recursions << " in recursive IP retrieval!";
335 break;
336 }
e18c1337 337 else
23f51766 338 {
fd62d09f
CH
339 min_cname_ttl = min(min_cname_ttl,
340 current_cname.Ttl.get_updated_value());
dbe986b9 341 result = get_ips(current_host, check_up_to_date);
23f51766
CH
342 }
343 }
344
345 GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
346 << result.size() << "-list after " << n_recursions
347 << " recursions";
348
349 // adjust ttl to min of ttl and min_cname_ttl
350 if (n_recursions > 0)
351 {
352 TimeToLive cname_ttl(min_cname_ttl);
353
354 BOOST_FOREACH( HostAddress &addr, result )
355 {
fd62d09f 356 if (addr.get_ttl().get_updated_value() > min_cname_ttl)
8d26221d
CH
357 {
358 GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
23f51766 359 addr.set_ttl(cname_ttl);
8d26221d 360 }
23f51766 361 }
e18c1337 362 }
23f51766 363
e18c1337
CH
364 return result;
365}
ad83004d 366
23f51766 367/**
9d1b2726
CH
368 * from a list of CNAMEs find the first one that is out of date or empty
369 *
370 * returns the hostname that is out of date or empty if all CNAMEs are
371 * up-to-date
23f51766
CH
372 *
373 * required in ResolverBase::get_skipper
23f51766
CH
374 */
375std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
376 const uint32_t ttl_thresh)
377{
8d26221d 378 std::string first_outdated = hostname;
23f51766
CH
379 Cname cname;
380 int n_recursions = 0;
381 while (true)
382 {
383 if (++n_recursions >= Config::MaxRetrievalRecursions)
384 {
385 GlobalLogger.warning() << "DnsCache: reached recursion limit of "
386 << n_recursions << " in search of outdated CNAMEs!";
9d1b2726
CH
387 return first_outdated; // not really out of date but currently
388 } // our best guess
23f51766 389
8d26221d
CH
390 cname = get_cname(first_outdated);
391 if (cname.Host.empty())
9d1b2726
CH
392 // reached end of cname list --> everything was up-to-date
393 return "";
23f51766
CH
394 else if (cname.Ttl.get_updated_value() > ttl_thresh)
395 // cname is up to date --> continue looking
8d26221d 396 first_outdated = cname.Host;
23f51766 397 else
9d1b2726
CH
398 // cname is out of date --> return its target
399 return cname.Host;
23f51766 400 }
9d1b2726
CH
401 // reach this point only if cname chain does not end with an IP
402 // --> all are up-to-date
403 return "";
23f51766 404}