1#!/usr/bin/env python 2# Copyright 2009 Google Inc. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Module for all nameserver related activity.""" 17 18__author__ = 'tstromberg@google.com (Thomas Stromberg)' 19 20import random 21import re 22import socket 23import sys 24import time 25 26 27if __name__ == '__main__': 28 sys.path.append('../nb_third_party') 29 30# external dependencies (from nb_third_party) 31import dns.exception 32import dns.message 33import dns.name 34import dns.query 35import dns.rcode 36import dns.rdataclass 37import dns.rdatatype 38import dns.resolver 39import dns.reversename 40import dns.version 41 42import health_checks 43import util 44 45# Look for buggy system versions of namebench 46if dns.version.hexversion < 17301744: 47 raise ValueError('dnspython 1.8.0+ required, while only %s was found. The ' 48 'namebench source bundles 1.8.0, so use it.' % dns.version.version) 49 50 51 52# How many failures before we disable system nameservers 53MAX_NORMAL_FAILURES = 2 54MAX_SYSTEM_FAILURES = 7 55MAX_PREFERRED_FAILURES = 5 56MAX_WARNINGS = 7 57 58FAILURE_PRONE_RATE = 10 59 60 61def _DoesClockGoBackwards(): 62 """Detect buggy Windows systems where time.clock goes backwards""" 63 reference = 0 64 print "Checking if time.clock() goes backwards (broken hardware)..." 65 for x in range(0, 200): 66 counter = time.clock() 67 if counter < reference: 68 print "Clock went backwards by %fms" % (counter - reference) 69 return True 70 reference = counter 71 time.sleep(random.random() / 500) 72 return False 73 74def _GetBestTimer(): 75 """Pick the most accurate timer for a platform.""" 76 if sys.platform[:3] == 'win' and not _DoesClockGoBackwards(): 77 return time.clock 78 else: 79 return time.time 80 81 82# EVIL IMPORT-TIME SIDE EFFECT 83BEST_TIMER_FUNCTION = _GetBestTimer() 84 85 86def ResponseToAscii(response): 87 if not response: 88 return None 89 if response.answer: 90 answers = [', '.join(map(str, x.items)) for x in response.answer] 91 return ' -> '.join(answers).rstrip('"').lstrip('"') 92 else: 93 return dns.rcode.to_text(response.rcode()) 94 95 96class BrokenSystemClock(Exception): 97 """Used if we detect errors with the system clock.""" 98 def __init__(self, value): 99 self.value = value 100 101 def __str__(self): 102 return repr(self.value) 103 104 105class NameServer(health_checks.NameServerHealthChecks): 106 """Hold information about a particular nameserver.""" 107 108 def __init__(self, ip, name=None, internal=False, preferred=False): 109 self.name = name 110 # We use _ for IPV6 representation in our configuration due to ConfigParser issues. 111 self.ip = ip.replace('_', ':') 112 self.is_system = internal 113 self.is_regional = False 114 self.is_global = False 115 self.is_custom = False 116 self.system_position = None 117 self.is_preferred = preferred 118 self.timeout = 5 119 self.health_timeout = 5 120 self.ping_timeout = 1 121 self.ResetTestStatus() 122 self.port_behavior = None 123 self._version = None 124 self._node_ids = set() 125 self._hostname = None 126 self.timer = BEST_TIMER_FUNCTION 127 128 if ':' in self.ip: 129 self.is_ipv6 = True 130 else: 131 self.is_ipv6 = False 132 133 @property 134 def check_average(self): 135 # If we only have a ping result, sort by it. Otherwise, use all non-ping results. 136 if len(self.checks) == 1: 137 return self.checks[0][3] 138 else: 139 return util.CalculateListAverage([x[3] for x in self.checks[1:]]) 140 141 @property 142 def fastest_check_duration(self): 143 if self.checks: 144 return min([x[3] for x in self.checks]) 145 else: 146 return 0.0 147 148 @property 149 def slowest_check_duration(self): 150 if self.checks: 151 return max([x[3] for x in self.checks]) 152 else: 153 return None 154 155 @property 156 def check_duration(self): 157 return sum([x[3] for x in self.checks]) 158 159 @property 160 def warnings_string(self): 161 if self.disabled: 162 return '(excluded: %s)' % self.disabled 163 else: 164 return ', '.join(map(str, self.warnings)) 165 166 @property 167 def warnings_comment(self): 168 if self.warnings or self.disabled: 169 return '# ' + self.warnings_string 170 else: 171 return '' 172 173 @property 174 def errors(self): 175 return ['%s (%s requests)' % (_[0], _[1]) for _ in self.error_map.items() if _[0] != 'Timeout'] 176 177 @property 178 def error_count(self): 179 return sum([_[1] for _ in self.error_map.items() if _[0] != 'Timeout']) 180 181 @property 182 def timeout_count(self): 183 return self.error_map.get('Timeout', 0) 184 185 @property 186 def notes(self): 187 """Return a list of notes about this nameserver object.""" 188 my_notes = [] 189 if self.system_position == 0: 190 my_notes.append('The current preferred DNS server.') 191 elif self.system_position: 192 my_notes.append('A backup DNS server for this system.') 193 if self.is_failure_prone: 194 my_notes.append('%0.0f queries to this host failed' % self.failure_rate) 195 if self.port_behavior and 'POOR' in self.port_behavior: 196 my_notes.append('Vulnerable to poisoning attacks (poor port diversity)') 197 if self.disabled: 198 my_notes.append(self.disabled) 199 else: 200 my_notes.extend(self.warnings) 201 if self.errors: 202 my_notes.extend(self.errors) 203 return my_notes 204 205 @property 206 def hostname(self): 207 if self._hostname is None: 208 self._hostname = self.RequestReverseIP(self.ip) 209 return self._hostname 210 211 @property 212 def version(self): 213 if self._version is None: 214 self.RequestVersion() 215 return self._version 216 217 @property 218 def node_ids(self): 219 """Return a set of node_ids seen on this system.""" 220 # We use a slightly different pattern here because we want to 221 # append to our results each time this is called. 222 self.RequestNodeId() 223 # Only return non-blank entries 224 return [x for x in self._node_ids if x] 225 226 @property 227 def partial_node_ids(self): 228 partials = [] 229 for node_id in self._node_ids: 230 node_bits = node_id.split('.') 231 if len(node_bits) >= 3: 232 partials.append('.'.join(node_bits[0:-2])) 233 else: 234 partials.append('.'.join(node_bits)) 235 return partials 236 237 @property 238 def name_and_node(self): 239 if self.node_ids: 240 return '%s [%s]' % (self.name, ', '.join(self.partial_node_ids)) 241 else: 242 return self.name 243 244 @property 245 def is_failure_prone(self): 246 if self.failure_rate >= FAILURE_PRONE_RATE: 247 return True 248 else: 249 return False 250 251 @property 252 def failure_rate(self): 253 if not self.failure_count or not self.request_count: 254 return 0 255 else: 256 return (float(self.failure_count) / float(self.request_count)) * 100 257 258 def __str__(self): 259 return '%s [%s]' % (self.name, self.ip) 260 261 def __repr__(self): 262 return self.__str__() 263 264 def ResetTestStatus(self): 265 """Reset testing status of this host.""" 266 self.warnings = set() 267 self.shared_with = set() 268 self.disabled = False 269 self.checks = [] 270 self.failed_test_count = 0 271 self.share_check_count = 0 272 self.cache_checks = [] 273 self.is_slower_replica = False 274 self.ResetErrorCounts() 275 276 def ResetErrorCounts(self): 277 """NOTE: This gets called by benchmark.Run()!""" 278 279 self.request_count = 0 280 self.failure_count = 0 281 self.error_map = {} 282 283 def AddFailure(self, message, fatal=False): 284 """Add a failure for this nameserver. This will effectively disable it's use.""" 285 if self.is_system: 286 max_count = MAX_SYSTEM_FAILURES 287 elif self.is_preferred: 288 max_count = MAX_PREFERRED_FAILURES 289 else: 290 max_count = MAX_NORMAL_FAILURES 291 292 self.failed_test_count += 1 293 294 if self.is_system or self.is_preferred: 295 # If the preferred host is IPv6 and we have no previous checks, fail quietly. 296 if self.is_ipv6 and len(self.checks) <= 1: 297 self.disabled = message 298 else: 299 print "\n* %s failed test #%s/%s: %s" % (self, self.failed_test_count, max_count, message) 300 301 # Fatal doesn't count for system & preferred nameservers. 302 if fatal and not (self.is_system or self.is_preferred): 303 self.disabled = message 304 elif self.failed_test_count >= max_count: 305 self.disabled = "Failed %s tests, last: %s" % (self.failed_test_count, message) 306 307 def AddWarning(self, message, penalty=True): 308 """Add a warning to a host.""" 309 310 if not isinstance(message, str): 311 print "Tried to add %s to %s (not a string)" % (message, self) 312 return None 313 314 self.warnings.add(message) 315 if penalty and len(self.warnings) >= MAX_WARNINGS: 316 self.AddFailure('Too many warnings (%s), probably broken.' % len(self.warnings), fatal=True) 317 318 def CreateRequest(self, record, request_type, return_type): 319 """Function to work around any dnspython make_query quirks.""" 320 return dns.message.make_query(record, request_type, return_type) 321 322 def Query(self, request, timeout): 323 return dns.query.udp(request, self.ip, timeout, 53) 324 325 def TimedRequest(self, type_string, record_string, timeout=None, rdataclass=None): 326 """Make a DNS request, returning the reply and duration it took. 327 328 Args: 329 type_string: DNS record type to query (string) 330 record_string: DNS record name to query (string) 331 timeout: optional timeout (float) 332 rdataclass: optional result class (defaults to rdataclass.IN) 333 334 Returns: 335 A tuple of (response, duration in ms [float], error_msg) 336 337 In the case of a DNS response timeout, the response object will be None. 338 """ 339 if not rdataclass: 340 rdataclass = dns.rdataclass.IN 341 else: 342 rdataclass = dns.rdataclass.from_text(rdataclass) 343 344 request_type = dns.rdatatype.from_text(type_string) 345 record = dns.name.from_text(record_string, None) 346 request = None 347 self.request_count += 1 348 349 # Sometimes it takes great effort just to craft a UDP packet. 350 try: 351 request = self.CreateRequest(record, request_type, rdataclass) 352 except ValueError, exc: 353 if not request: 354 return (None, 0, util.GetLastExceptionString()) 355 356 if not timeout: 357 timeout = self.timeout 358 359 error_msg = None 360 exc = None 361 duration = None 362 try: 363 start_time = self.timer() 364 response = self.Query(request, timeout) 365 duration = self.timer() - start_time 366 except (dns.exception.Timeout), exc: 367 response = None 368 except (dns.query.BadResponse, dns.message.TrailingJunk, 369 dns.query.UnexpectedSource), exc: 370 error_msg = util.GetLastExceptionString() 371 response = None 372 # This is pretty normal if someone runs namebench offline. 373 except socket.error: 374 response = None 375 if ':' in self.ip: 376 error_msg = 'socket error: IPv6 may not be available.' 377 else: 378 error_msg = util.GetLastExceptionString() 379 # Pass these exceptions up the food chain 380 except (KeyboardInterrupt, SystemExit, SystemError), exc: 381 raise exc 382 except: 383 error_msg = util.GetLastExceptionString() 384 print "* Unusual error with %s:%s on %s: %s" % (type_string, record_string, self, error_msg) 385 response = None 386 387 if not response: 388 self.failure_count += 1 389 390 if not duration: 391 duration = self.timer() - start_time 392 393 if exc and not error_msg: 394 error_msg = '%s: %s' % (record_string, util.GetLastExceptionString()) 395 396 if error_msg: 397 key = util.GetLastExceptionString() 398 self.error_map[key] = self.error_map.setdefault(key, 0) + 1 399 400 if duration < 0: 401 raise BrokenSystemClock('The time on your machine appears to be going backwards. ' 402 'We cannot accurately benchmark due to this error. ' 403 '(timer=%s, duration=%s)' % (self.timer, duration)) 404 return (response, util.SecondsToMilliseconds(duration), error_msg) 405 406 def RequestVersion(self): 407 version = '' 408 (response, duration, error_msg) = self.TimedRequest('TXT', 'version.bind.', rdataclass='CHAOS', 409 timeout=self.health_timeout*2) 410 if response and response.answer: 411 response_string = ResponseToAscii(response) 412 if (re.search('\d', response_string) or 413 (re.search('recursive|ns|server|bind|unbound', response_string, re.I) 414 and 'ontact' not in response_string and '...' not in response_string)): 415 version = response_string 416 self._version = version 417 return (version, duration, error_msg) 418 419 def RequestReverseIP(self, ip): 420 """Request a hostname for a given IP address.""" 421 try: 422 answer = dns.resolver.query(dns.reversename.from_address(ip), 'PTR') 423 except: 424 answer = None 425 if answer: 426 return answer[0].to_text().rstrip('.') 427 else: 428 return ip 429 430 def RequestNodeId(self): 431 """Try to determine the node id for this nameserver (tries many methods).""" 432 node = '' 433 rdataclass = None 434 reverse_lookup = False 435 436 if self.hostname.endswith('ultradns.net') or self.ip.startswith('156.154.7'): 437 query_type, record_name = ('A', 'whoareyou.ultradns.net.') 438 reverse_lookup = True 439 elif self.ip.startswith('8.8'): 440 query_type, record_name = ('A', 'self.myresolver.info.') 441 reverse_lookup = True 442 elif self.hostname.endswith('opendns.com') or self.ip.startswith('208.67.22'): 443 query_type, record_name = ('TXT', 'which.opendns.com.') 444 else: 445 query_type, record_name, rdataclass = ('TXT', 'hostname.bind.', 'CHAOS') 446 447 (response, duration, error_msg) = self.TimedRequest(query_type, record_name, rdataclass=rdataclass, 448 timeout=self.health_timeout*2) 449 if not response or not response.answer: 450 query_type, record_name, rdataclass = ('TXT', 'id.server.', 'CHAOS') 451 (response, duration, error_msg) = self.TimedRequest(query_type, record_name, rdataclass=rdataclass, 452 timeout=self.health_timeout*2) 453 454 if response and response.answer: 455 node = ResponseToAscii(response) 456 if reverse_lookup: 457 node = self.RequestReverseIP(node) 458 459 # This is what the .node* properties use. 460 self._node_ids.add(node) 461 return (node, duration, error_msg) 462 463 464if __name__ == '__main__': 465 ns = NameServer(sys.argv[1]) 466 print "-" * 64 467 print "IP: %s" % ns.ip 468 print "Host: %s" % ns.hostname 469 print "Version: %s" % ns.version 470 print "Node: %s" % ns.node_ids 471