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