Commit | Line | Data |
---|---|---|
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 |
41 | using boost::bind; |
42 | using boost::posix_time::seconds; | |
43 | using I2n::Logger::GlobalLogger; | |
44 | ||
45 | namespace Config | |
46 | { | |
47 | int SaveTimerSeconds = 60; | |
dbe986b9 | 48 | int MaxRetrievalRecursions = 10; |
96779587 CH |
49 | } |
50 | ||
23f51766 CH |
51 | Cname::Cname() |
52 | : Host() | |
53 | , Ttl() | |
54 | {} | |
55 | ||
56 | Cname::Cname(const std::string &host, const uint32_t ttl) | |
57 | : Host( host ) | |
58 | , Ttl( ttl ) | |
59 | {} | |
60 | ||
26b0f687 CH |
61 | Cname::Cname(const std::string &host, const TimeToLive &ttl) |
62 | : Host( host ) | |
63 | , Ttl( ttl ) | |
64 | {} | |
65 | ||
96779587 CH |
66 | DnsCache::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 | ||
84 | DnsCache::~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 | ||
96 | void 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 | ||
122 | void 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 |
152 | void 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 | |
188 | std::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 | 204 | void 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 | 221 | void 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 |
246 | HostAddressVec 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 | */ | |
281 | Cname 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 | |
308 | HostAddressVec 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 | */ | |
366 | std::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 |