|
@@ -0,0 +1,1225 @@
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+import StringIO
|
|
|
|
+import string
|
|
|
|
+import re
|
|
|
|
+import datetime
|
|
|
|
+import gzip
|
|
|
|
+import os.path
|
|
|
|
+import json
|
|
|
|
+import math
|
|
|
|
+import sys
|
|
|
|
+import urllib
|
|
|
|
+import urllib2
|
|
|
|
+import hashlib
|
|
|
|
+import dateutil.parser
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+import logging
|
|
|
|
+logging.basicConfig(level=logging.INFO)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+OUTPUT_CANDIDATES = False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ONIONOO = 'https://onionoo.torproject.org/'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+LOCAL_FILES_ONLY = False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+INCLUDE_UNLISTED_ENTRIES = True if OUTPUT_CANDIDATES else False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+BLACKLIST_EXCLUDES_WHITELIST_ENTRIES = True
|
|
|
|
+
|
|
|
|
+WHITELIST_FILE_NAME = 'scripts/maint/fallback.whitelist'
|
|
|
|
+BLACKLIST_FILE_NAME = 'scripts/maint/fallback.blacklist'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+MAX_LIST_FILE_SIZE = 1024 * 1024
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ADDRESS_AND_PORT_STABLE_DAYS = 120
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+CUTOFF_RUNNING = .95
|
|
|
|
+CUTOFF_V2DIR = .95
|
|
|
|
+CUTOFF_GUARD = .95
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+PERMITTED_BADEXIT = .00
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+FALLBACK_PROPORTION_OF_GUARDS = None if OUTPUT_CANDIDATES else 0.2
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+MAX_FALLBACK_COUNT = 500
|
|
|
|
+
|
|
|
|
+MIN_FALLBACK_COUNT = 100
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+EXIT_WEIGHT_FRACTION = 0.2
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+STRICT_FALLBACK_WEIGHTS = False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+TARGET_MAX_WEIGHT_FRACTION = 1/10.0
|
|
|
|
+REWEIGHTING_FUDGE_FACTOR = 0.8
|
|
|
|
+MAX_WEIGHT_FRACTION = TARGET_MAX_WEIGHT_FRACTION * REWEIGHTING_FUDGE_FACTOR
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+MIN_WEIGHT_FRACTION = 0.0 if OUTPUT_CANDIDATES else 1/1000.0
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+AGE_ALPHA = 0.99
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ONIONOO_SCALE_ONE = 999.
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def parse_ts(t):
|
|
|
|
+ return datetime.datetime.strptime(t, "%Y-%m-%d %H:%M:%S")
|
|
|
|
+
|
|
|
|
+def remove_bad_chars(raw_string, bad_char_list):
|
|
|
|
+
|
|
|
|
+ escaped_string = raw_string
|
|
|
|
+ for c in bad_char_list:
|
|
|
|
+ escaped_string = escaped_string.replace(c, '')
|
|
|
|
+ return escaped_string
|
|
|
|
+
|
|
|
|
+def cleanse_whitespace(raw_string):
|
|
|
|
+
|
|
|
|
+ escaped_string = raw_string
|
|
|
|
+ for c in string.whitespace:
|
|
|
|
+ escaped_string = escaped_string.replace(c, ' ')
|
|
|
|
+ return escaped_string
|
|
|
|
+
|
|
|
|
+def cleanse_c_multiline_comment(raw_string):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ bad_char_list = '*'
|
|
|
|
+
|
|
|
|
+ bad_char_list += '\0'
|
|
|
|
+
|
|
|
|
+ escaped_string = remove_bad_chars(raw_string, bad_char_list)
|
|
|
|
+
|
|
|
|
+ escaped_string = cleanse_whitespace(escaped_string)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ return escaped_string
|
|
|
|
+
|
|
|
|
+def cleanse_c_string(raw_string):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ bad_char_list = '"'
|
|
|
|
+
|
|
|
|
+ bad_char_list += '\\'
|
|
|
|
+
|
|
|
|
+ bad_char_list += '\0'
|
|
|
|
+
|
|
|
|
+ escaped_string = remove_bad_chars(raw_string, bad_char_list)
|
|
|
|
+
|
|
|
|
+ escaped_string = cleanse_whitespace(escaped_string)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ return escaped_string
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+fetch_source = {}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def register_fetch_source(what, url, relays_published, version):
|
|
|
|
+ fetch_source[what] = {}
|
|
|
|
+ fetch_source[what]['url'] = url
|
|
|
|
+ fetch_source[what]['relays_published'] = relays_published
|
|
|
|
+ fetch_source[what]['version'] = version
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def fetch_source_list():
|
|
|
|
+ return sorted(fetch_source.keys())
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def describe_fetch_source(what):
|
|
|
|
+ desc = '/*'
|
|
|
|
+ desc += '\n'
|
|
|
|
+ desc += 'Onionoo Source: '
|
|
|
|
+ desc += cleanse_c_multiline_comment(what)
|
|
|
|
+ desc += ' Date: '
|
|
|
|
+ desc += cleanse_c_multiline_comment(fetch_source[what]['relays_published'])
|
|
|
|
+ desc += ' Version: '
|
|
|
|
+ desc += cleanse_c_multiline_comment(fetch_source[what]['version'])
|
|
|
|
+ desc += '\n'
|
|
|
|
+ desc += 'URL: '
|
|
|
|
+ desc += cleanse_c_multiline_comment(fetch_source[what]['url'])
|
|
|
|
+ desc += '\n'
|
|
|
|
+ desc += '*/'
|
|
|
|
+ return desc
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def write_to_file(str, file_name, max_len):
|
|
|
|
+ try:
|
|
|
|
+ with open(file_name, 'w') as f:
|
|
|
|
+ f.write(str[0:max_len])
|
|
|
|
+ except EnvironmentError, error:
|
|
|
|
+ logging.debug('Writing file %s failed: %d: %s'%
|
|
|
|
+ (file_name,
|
|
|
|
+ error.errno,
|
|
|
|
+ error.strerror)
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+def read_from_file(file_name, max_len):
|
|
|
|
+ try:
|
|
|
|
+ if os.path.isfile(file_name):
|
|
|
|
+ with open(file_name, 'r') as f:
|
|
|
|
+ return f.read(max_len)
|
|
|
|
+ except EnvironmentError, error:
|
|
|
|
+ logging.debug('Loading file %s failed: %d: %s'%
|
|
|
|
+ (file_name,
|
|
|
|
+ error.errno,
|
|
|
|
+ error.strerror)
|
|
|
|
+ )
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+def load_possibly_compressed_response_json(response):
|
|
|
|
+ if response.info().get('Content-Encoding') == 'gzip':
|
|
|
|
+ buf = StringIO.StringIO( response.read() )
|
|
|
|
+ f = gzip.GzipFile(fileobj=buf)
|
|
|
|
+ return json.load(f)
|
|
|
|
+ else:
|
|
|
|
+ return json.load(response)
|
|
|
|
+
|
|
|
|
+def load_json_from_file(json_file_name):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ with open(json_file_name, 'r') as f:
|
|
|
|
+ return json.load(f)
|
|
|
|
+ except EnvironmentError, error:
|
|
|
|
+ raise Exception('Reading not-modified json file %s failed: %d: %s'%
|
|
|
|
+ (json_file_name,
|
|
|
|
+ error.errno,
|
|
|
|
+ error.strerror)
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def onionoo_fetch(what, **kwargs):
|
|
|
|
+ params = kwargs
|
|
|
|
+ params['type'] = 'relay'
|
|
|
|
+
|
|
|
|
+ params['first_seen_days'] = '%d-'%(ADDRESS_AND_PORT_STABLE_DAYS,)
|
|
|
|
+ params['last_seen_days'] = '-7'
|
|
|
|
+ params['flag'] = 'V2Dir'
|
|
|
|
+ url = ONIONOO + what + '?' + urllib.urlencode(params)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ base_file_name = what + '-' + hashlib.sha1(url).hexdigest()
|
|
|
|
+
|
|
|
|
+ full_url_file_name = base_file_name + '.full_url'
|
|
|
|
+ MAX_FULL_URL_LENGTH = 1024
|
|
|
|
+
|
|
|
|
+ last_modified_file_name = base_file_name + '.last_modified'
|
|
|
|
+ MAX_LAST_MODIFIED_LENGTH = 64
|
|
|
|
+
|
|
|
|
+ json_file_name = base_file_name + '.json'
|
|
|
|
+
|
|
|
|
+ if LOCAL_FILES_ONLY:
|
|
|
|
+
|
|
|
|
+ response_json = load_json_from_file(json_file_name)
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ write_to_file(url, full_url_file_name, MAX_FULL_URL_LENGTH)
|
|
|
|
+
|
|
|
|
+ request = urllib2.Request(url)
|
|
|
|
+ request.add_header('Accept-encoding', 'gzip')
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ last_mod_date = read_from_file(last_modified_file_name,
|
|
|
|
+ MAX_LAST_MODIFIED_LENGTH)
|
|
|
|
+ if last_mod_date is not None:
|
|
|
|
+ request.add_header('If-modified-since', last_mod_date)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if last_mod_date is not None:
|
|
|
|
+ last_mod = dateutil.parser.parse(last_mod_date)
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ last_mod = datetime.datetime.utcfromtimestamp(0)
|
|
|
|
+
|
|
|
|
+ last_mod = last_mod.replace(tzinfo=None)
|
|
|
|
+
|
|
|
|
+ response_code = 0
|
|
|
|
+ try:
|
|
|
|
+ response = urllib2.urlopen(request)
|
|
|
|
+ response_code = response.getcode()
|
|
|
|
+ except urllib2.HTTPError, error:
|
|
|
|
+ response_code = error.code
|
|
|
|
+
|
|
|
|
+ six_hours_ago = datetime.datetime.utcnow()
|
|
|
|
+ six_hours_ago = six_hours_ago.replace(tzinfo=None)
|
|
|
|
+ six_hours_ago -= datetime.timedelta(hours=6)
|
|
|
|
+
|
|
|
|
+ if response_code == 304:
|
|
|
|
+ if last_mod < six_hours_ago:
|
|
|
|
+ raise Exception("Outdated data from " + url + ": "
|
|
|
|
+ + str(error.code) + ": " + error.reason)
|
|
|
|
+ else:
|
|
|
|
+ pass
|
|
|
|
+ else:
|
|
|
|
+ raise Exception("Could not get " + url + ": "
|
|
|
|
+ + str(error.code) + ": " + error.reason)
|
|
|
|
+
|
|
|
|
+ if response_code == 200:
|
|
|
|
+
|
|
|
|
+ response_json = load_possibly_compressed_response_json(response)
|
|
|
|
+
|
|
|
|
+ with open(json_file_name, 'w') as f:
|
|
|
|
+
|
|
|
|
+ json.dump(response_json, f, separators=(',',':'))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if response.info().get('Last-modified') is not None:
|
|
|
|
+ write_to_file(response.info().get('Last-Modified'),
|
|
|
|
+ last_modified_file_name,
|
|
|
|
+ MAX_LAST_MODIFIED_LENGTH)
|
|
|
|
+
|
|
|
|
+ elif response_code == 304:
|
|
|
|
+
|
|
|
|
+ response_json = load_json_from_file(json_file_name)
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ raise Exception("Unexpected HTTP response code to " + url + ": "
|
|
|
|
+ + str(response_code))
|
|
|
|
+
|
|
|
|
+ register_fetch_source(what,
|
|
|
|
+ url,
|
|
|
|
+ response_json['relays_published'],
|
|
|
|
+ response_json['version'])
|
|
|
|
+
|
|
|
|
+ return response_json
|
|
|
|
+
|
|
|
|
+def fetch(what, **kwargs):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ return onionoo_fetch(what, **kwargs)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class Candidate(object):
|
|
|
|
+ CUTOFF_ADDRESS_AND_PORT_STABLE = (datetime.datetime.now()
|
|
|
|
+ - datetime.timedelta(ADDRESS_AND_PORT_STABLE_DAYS))
|
|
|
|
+
|
|
|
|
+ def __init__(self, details):
|
|
|
|
+ for f in ['fingerprint', 'nickname', 'last_changed_address_or_port',
|
|
|
|
+ 'consensus_weight', 'or_addresses', 'dir_address']:
|
|
|
|
+ if not f in details: raise Exception("Document has no %s field."%(f,))
|
|
|
|
+
|
|
|
|
+ if not 'contact' in details:
|
|
|
|
+ details['contact'] = None
|
|
|
|
+ if not 'flags' in details or details['flags'] is None:
|
|
|
|
+ details['flags'] = []
|
|
|
|
+ details['last_changed_address_or_port'] = parse_ts(
|
|
|
|
+ details['last_changed_address_or_port'])
|
|
|
|
+ self._data = details
|
|
|
|
+ self._stable_sort_or_addresses()
|
|
|
|
+
|
|
|
|
+ self._fpr = self._data['fingerprint']
|
|
|
|
+ self._running = self._guard = self._v2dir = 0.
|
|
|
|
+ self._split_dirport()
|
|
|
|
+ self._compute_orport()
|
|
|
|
+ if self.orport is None:
|
|
|
|
+ raise Exception("Failed to get an orport for %s."%(self._fpr,))
|
|
|
|
+ self._compute_ipv6addr()
|
|
|
|
+ if self.ipv6addr is None:
|
|
|
|
+ logging.debug("Failed to get an ipv6 address for %s."%(self._fpr,))
|
|
|
|
+
|
|
|
|
+ if self.is_exit():
|
|
|
|
+ current_weight = self._data['consensus_weight']
|
|
|
|
+ exit_weight = current_weight * EXIT_WEIGHT_FRACTION
|
|
|
|
+ self._data['original_consensus_weight'] = current_weight
|
|
|
|
+ self._data['consensus_weight'] = exit_weight
|
|
|
|
+
|
|
|
|
+ def _stable_sort_or_addresses(self):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ self._data['or_addresses_raw'] = self._data['or_addresses']
|
|
|
|
+ or_address_primary = self._data['or_addresses'][:1]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ or_addresses_secondaries_stable = sorted(self._data['or_addresses'][1:])
|
|
|
|
+ or_addresses_stable = or_address_primary + or_addresses_secondaries_stable
|
|
|
|
+ self._data['or_addresses'] = or_addresses_stable
|
|
|
|
+
|
|
|
|
+ def get_fingerprint(self):
|
|
|
|
+ return self._fpr
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def is_valid_ipv4_address(address):
|
|
|
|
+ if not isinstance(address, (str, unicode)):
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if address.count(".") != 3:
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ for entry in address.split("."):
|
|
|
|
+ if not entry.isdigit() or int(entry) < 0 or int(entry) > 255:
|
|
|
|
+ return False
|
|
|
|
+ elif entry[0] == "0" and len(entry) > 1:
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def is_valid_ipv6_address(address):
|
|
|
|
+ if not isinstance(address, (str, unicode)):
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ address = address[1:-1]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ colon_count = address.count(":")
|
|
|
|
+
|
|
|
|
+ if colon_count > 7:
|
|
|
|
+ return False
|
|
|
|
+ elif colon_count != 7 and not "::" in address:
|
|
|
|
+ return False
|
|
|
|
+ elif address.count("::") > 1 or ":::" in address:
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+ found_ipv4_on_previous_entry = False
|
|
|
|
+ for entry in address.split(":"):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if found_ipv4_on_previous_entry:
|
|
|
|
+ return False
|
|
|
|
+ if not re.match("^[0-9a-fA-f]{0,4}$", entry):
|
|
|
|
+ if not Candidate.is_valid_ipv4_address(entry):
|
|
|
|
+ return False
|
|
|
|
+ else:
|
|
|
|
+ found_ipv4_on_previous_entry = True
|
|
|
|
+
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ def _split_dirport(self):
|
|
|
|
+
|
|
|
|
+ (self.dirip, _dirport) = self._data['dir_address'].split(':', 2)
|
|
|
|
+ self.dirport = int(_dirport)
|
|
|
|
+
|
|
|
|
+ def _compute_orport(self):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ self._split_dirport()
|
|
|
|
+ self.orport = None
|
|
|
|
+ for i in self._data['or_addresses']:
|
|
|
|
+ if i != self._data['or_addresses'][0]:
|
|
|
|
+ logging.debug('Secondary IPv4 Address Used for %s: %s'%(self._fpr, i))
|
|
|
|
+ (ipaddr, port) = i.rsplit(':', 1)
|
|
|
|
+ if (ipaddr == self.dirip) and Candidate.is_valid_ipv4_address(ipaddr):
|
|
|
|
+ self.orport = int(port)
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ def _compute_ipv6addr(self):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ self.ipv6addr = None
|
|
|
|
+ self.ipv6orport = None
|
|
|
|
+
|
|
|
|
+ for i in self._data['or_addresses']:
|
|
|
|
+ (ipaddr, port) = i.rsplit(':', 1)
|
|
|
|
+ if (port == self.orport) and Candidate.is_valid_ipv6_address(ipaddr):
|
|
|
|
+ self.ipv6addr = ipaddr
|
|
|
|
+ self.ipv6orport = port
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ for i in self._data['or_addresses']:
|
|
|
|
+ (ipaddr, port) = i.rsplit(':', 1)
|
|
|
|
+ if Candidate.is_valid_ipv6_address(ipaddr):
|
|
|
|
+ self.ipv6addr = ipaddr
|
|
|
|
+ self.ipv6orport = port
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def _extract_generic_history(history, which='unknown'):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ generic_history = []
|
|
|
|
+
|
|
|
|
+ periods = history.keys()
|
|
|
|
+ periods.sort(key = lambda x: history[x]['interval'])
|
|
|
|
+ now = datetime.datetime.now()
|
|
|
|
+ newest = now
|
|
|
|
+ for p in periods:
|
|
|
|
+ h = history[p]
|
|
|
|
+ interval = datetime.timedelta(seconds = h['interval'])
|
|
|
|
+ this_ts = parse_ts(h['last'])
|
|
|
|
+
|
|
|
|
+ if (len(h['values']) != h['count']):
|
|
|
|
+ logging.warn('Inconsistent value count in %s document for %s'
|
|
|
|
+ %(p, which))
|
|
|
|
+ for v in reversed(h['values']):
|
|
|
|
+ if (this_ts <= newest):
|
|
|
|
+ generic_history.append(
|
|
|
|
+ { 'age': (now - this_ts).total_seconds(),
|
|
|
|
+ 'length': interval.total_seconds(),
|
|
|
|
+ 'value': v
|
|
|
|
+ })
|
|
|
|
+ newest = this_ts
|
|
|
|
+ this_ts -= interval
|
|
|
|
+
|
|
|
|
+ if (this_ts + interval != parse_ts(h['first'])):
|
|
|
|
+ logging.warn('Inconsistent time information in %s document for %s'
|
|
|
|
+ %(p, which))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ return generic_history
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def _avg_generic_history(generic_history):
|
|
|
|
+ a = []
|
|
|
|
+ for i in generic_history:
|
|
|
|
+ if (i['length'] is not None
|
|
|
|
+ and i['age'] is not None
|
|
|
|
+ and i['value'] is not None):
|
|
|
|
+ w = i['length'] * math.pow(AGE_ALPHA, i['age']/(3600*24))
|
|
|
|
+ a.append( (i['value'] * w, w) )
|
|
|
|
+
|
|
|
|
+ sv = math.fsum(map(lambda x: x[0], a))
|
|
|
|
+ sw = math.fsum(map(lambda x: x[1], a))
|
|
|
|
+
|
|
|
|
+ return sv/sw
|
|
|
|
+
|
|
|
|
+ def _add_generic_history(self, history):
|
|
|
|
+ periods = r['read_history'].keys()
|
|
|
|
+ periods.sort(key = lambda x: r['read_history'][x]['interval'] )
|
|
|
|
+
|
|
|
|
+ print periods
|
|
|
|
+
|
|
|
|
+ def add_running_history(self, history):
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+ def add_uptime(self, uptime):
|
|
|
|
+ logging.debug('Adding uptime %s.'%(self._fpr,))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if not 'flags' in uptime:
|
|
|
|
+ logging.debug('No flags in document for %s.'%(self._fpr,))
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ for f in ['Running', 'Guard', 'V2Dir']:
|
|
|
|
+ if not f in uptime['flags']:
|
|
|
|
+ logging.debug('No %s in flags for %s.'%(f, self._fpr,))
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ running = self._extract_generic_history(uptime['flags']['Running'],
|
|
|
|
+ '%s-Running'%(self._fpr))
|
|
|
|
+ guard = self._extract_generic_history(uptime['flags']['Guard'],
|
|
|
|
+ '%s-Guard'%(self._fpr))
|
|
|
|
+ v2dir = self._extract_generic_history(uptime['flags']['V2Dir'],
|
|
|
|
+ '%s-V2Dir'%(self._fpr))
|
|
|
|
+ if 'BadExit' in uptime['flags']:
|
|
|
|
+ badexit = self._extract_generic_history(uptime['flags']['BadExit'],
|
|
|
|
+ '%s-BadExit'%(self._fpr))
|
|
|
|
+
|
|
|
|
+ self._running = self._avg_generic_history(running) / ONIONOO_SCALE_ONE
|
|
|
|
+ self._guard = self._avg_generic_history(guard) / ONIONOO_SCALE_ONE
|
|
|
|
+ self._v2dir = self._avg_generic_history(v2dir) / ONIONOO_SCALE_ONE
|
|
|
|
+ self._badexit = None
|
|
|
|
+ if 'BadExit' in uptime['flags']:
|
|
|
|
+ self._badexit = self._avg_generic_history(badexit) / ONIONOO_SCALE_ONE
|
|
|
|
+
|
|
|
|
+ def is_candidate(self):
|
|
|
|
+ if (self._data['last_changed_address_or_port'] >
|
|
|
|
+ self.CUTOFF_ADDRESS_AND_PORT_STABLE):
|
|
|
|
+ logging.debug('%s not a candidate: changed address/port recently (%s)',
|
|
|
|
+ self._fpr, self._data['last_changed_address_or_port'])
|
|
|
|
+ return False
|
|
|
|
+ if self._running < CUTOFF_RUNNING:
|
|
|
|
+ logging.debug('%s not a candidate: running avg too low (%lf)',
|
|
|
|
+ self._fpr, self._running)
|
|
|
|
+ return False
|
|
|
|
+ if self._guard < CUTOFF_GUARD:
|
|
|
|
+ logging.debug('%s not a candidate: guard avg too low (%lf)',
|
|
|
|
+ self._fpr, self._guard)
|
|
|
|
+ return False
|
|
|
|
+ if self._v2dir < CUTOFF_V2DIR:
|
|
|
|
+ logging.debug('%s not a candidate: v2dir avg too low (%lf)',
|
|
|
|
+ self._fpr, self._v2dir)
|
|
|
|
+ return False
|
|
|
|
+ if self._badexit is not None and self._badexit > PERMITTED_BADEXIT:
|
|
|
|
+ logging.debug('%s not a candidate: badexit avg too high (%lf)',
|
|
|
|
+ self._fpr, self._badexit)
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+ if (not self._data.has_key('recommended_version')
|
|
|
|
+ or not self._data['recommended_version']):
|
|
|
|
+ return False
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ def is_in_whitelist(self, relaylist):
|
|
|
|
+ """ A fallback matches if each key in the whitelist line matches:
|
|
|
|
+ ipv4
|
|
|
|
+ dirport
|
|
|
|
+ orport
|
|
|
|
+ id
|
|
|
|
+ ipv6 address and port (if present)
|
|
|
|
+ If the fallback has an ipv6 key, the whitelist line must also have
|
|
|
|
+ it, and vice versa, otherwise they don't match. """
|
|
|
|
+ for entry in relaylist:
|
|
|
|
+ if entry['ipv4'] != self.dirip:
|
|
|
|
+ continue
|
|
|
|
+ if int(entry['dirport']) != self.dirport:
|
|
|
|
+ continue
|
|
|
|
+ if int(entry['orport']) != self.orport:
|
|
|
|
+ continue
|
|
|
|
+ if entry['id'] != self._fpr:
|
|
|
|
+ continue
|
|
|
|
+ if (entry.has_key('ipv6')
|
|
|
|
+ and self.ipv6addr is not None and self.ipv6orport is not None):
|
|
|
|
+
|
|
|
|
+ if entry['ipv6'] != self.ipv6addr + ':' + self.ipv6orport:
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ elif entry.has_key('ipv6') and self.ipv6addr is None:
|
|
|
|
+ continue
|
|
|
|
+ elif not entry.has_key('ipv6') and self.ipv6addr is not None:
|
|
|
|
+ continue
|
|
|
|
+ return True
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+ def is_in_blacklist(self, relaylist):
|
|
|
|
+ """ A fallback matches a blacklist line if a sufficiently specific group
|
|
|
|
+ of attributes matches:
|
|
|
|
+ ipv4 & dirport
|
|
|
|
+ ipv4 & orport
|
|
|
|
+ id
|
|
|
|
+ ipv6 & dirport
|
|
|
|
+ ipv6 & ipv6 orport
|
|
|
|
+ If the fallback and the blacklist line both have an ipv6 key,
|
|
|
|
+ their values will be compared, otherwise, they will be ignored.
|
|
|
|
+ If there is no dirport and no orport, the entry matches all relays on
|
|
|
|
+ that ip. """
|
|
|
|
+ for entry in relaylist:
|
|
|
|
+ for key in entry:
|
|
|
|
+ value = entry[key]
|
|
|
|
+ if key == 'ipv4' and value == self.dirip:
|
|
|
|
+
|
|
|
|
+ if entry.has_key('dirport'):
|
|
|
|
+ if int(entry['dirport']) == self.dirport:
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ elif entry.has_key('orport'):
|
|
|
|
+ if int(entry['orport']) == self.orport:
|
|
|
|
+ return True
|
|
|
|
+ else:
|
|
|
|
+ return True
|
|
|
|
+ if key == 'id' and value == self._fpr:
|
|
|
|
+ return True
|
|
|
|
+ if (key == 'ipv6'
|
|
|
|
+ and self.ipv6addr is not None and self.ipv6orport is not None):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if value == self.ipv6addr + ':' + self.ipv6orport:
|
|
|
|
+
|
|
|
|
+ if entry.has_key('dirport'):
|
|
|
|
+ if int(entry['dirport']) == self.dirport:
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ elif entry.has_key('orport'):
|
|
|
|
+ if int(entry['orport']) == self.orport:
|
|
|
|
+ return True
|
|
|
|
+ else:
|
|
|
|
+ return True
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+ def is_exit(self):
|
|
|
|
+ return 'Exit' in self._data['flags']
|
|
|
|
+
|
|
|
|
+ def is_guard(self):
|
|
|
|
+ return 'Guard' in self._data['flags']
|
|
|
|
+
|
|
|
|
+ def fallback_weight_fraction(self, total_weight):
|
|
|
|
+ return float(self._data['consensus_weight']) / total_weight
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def original_consensus_weight(self):
|
|
|
|
+ if self._data.has_key('original_consensus_weight'):
|
|
|
|
+ return self._data['original_consensus_weight']
|
|
|
|
+ else:
|
|
|
|
+ return self._data['consensus_weight']
|
|
|
|
+
|
|
|
|
+ def original_fallback_weight_fraction(self, total_weight):
|
|
|
|
+ return float(self.original_consensus_weight()) / total_weight
|
|
|
|
+
|
|
|
|
+ def fallbackdir_line(self, total_weight, original_total_weight):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ s = '/*'
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += cleanse_c_multiline_comment(self._data['nickname'])
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += 'Flags: '
|
|
|
|
+ s += cleanse_c_multiline_comment(' '.join(sorted(self._data['flags'])))
|
|
|
|
+ s += '\n'
|
|
|
|
+ weight = self._data['consensus_weight']
|
|
|
|
+ percent_weight = self.fallback_weight_fraction(total_weight)*100
|
|
|
|
+ s += 'Fallback Weight: %d / %d (%.3f%%)'%(weight, total_weight,
|
|
|
|
+ percent_weight)
|
|
|
|
+ s += '\n'
|
|
|
|
+ o_weight = self.original_consensus_weight()
|
|
|
|
+ if o_weight != weight:
|
|
|
|
+ o_percent_weight = self.original_fallback_weight_fraction(
|
|
|
|
+ original_total_weight)*100
|
|
|
|
+ s += 'Consensus Weight: %d / %d (%.3f%%)'%(o_weight,
|
|
|
|
+ original_total_weight,
|
|
|
|
+ o_percent_weight)
|
|
|
|
+ s += '\n'
|
|
|
|
+ if self._data['contact'] is not None:
|
|
|
|
+ s += cleanse_c_multiline_comment(self._data['contact'])
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += '*/'
|
|
|
|
+ s += '\n'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ s += '"%s orport=%d id=%s"'%(
|
|
|
|
+ cleanse_c_string(self._data['dir_address']),
|
|
|
|
+ self.orport,
|
|
|
|
+ cleanse_c_string(self._fpr))
|
|
|
|
+ s += '\n'
|
|
|
|
+ if self.ipv6addr is not None:
|
|
|
|
+ s += '" ipv6=%s:%s"'%(
|
|
|
|
+ cleanse_c_string(self.ipv6addr), cleanse_c_string(self.ipv6orport))
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += '" weight=%d",'%(weight)
|
|
|
|
+ return s
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class CandidateList(dict):
|
|
|
|
+ def __init__(self):
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+ def _add_relay(self, details):
|
|
|
|
+ if not 'dir_address' in details: return
|
|
|
|
+ c = Candidate(details)
|
|
|
|
+ self[ c.get_fingerprint() ] = c
|
|
|
|
+
|
|
|
|
+ def _add_uptime(self, uptime):
|
|
|
|
+ try:
|
|
|
|
+ fpr = uptime['fingerprint']
|
|
|
|
+ except KeyError:
|
|
|
|
+ raise Exception("Document has no fingerprint field.")
|
|
|
|
+
|
|
|
|
+ try:
|
|
|
|
+ c = self[fpr]
|
|
|
|
+ except KeyError:
|
|
|
|
+ logging.debug('Got unknown relay %s in uptime document.'%(fpr,))
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ c.add_uptime(uptime)
|
|
|
|
+
|
|
|
|
+ def _add_details(self):
|
|
|
|
+ logging.debug('Loading details document.')
|
|
|
|
+ d = fetch('details',
|
|
|
|
+ fields=('fingerprint,nickname,contact,last_changed_address_or_port,' +
|
|
|
|
+ 'consensus_weight,or_addresses,dir_address,' +
|
|
|
|
+ 'recommended_version,flags'))
|
|
|
|
+ logging.debug('Loading details document done.')
|
|
|
|
+
|
|
|
|
+ if not 'relays' in d: raise Exception("No relays found in document.")
|
|
|
|
+
|
|
|
|
+ for r in d['relays']: self._add_relay(r)
|
|
|
|
+
|
|
|
|
+ def _add_uptimes(self):
|
|
|
|
+ logging.debug('Loading uptime document.')
|
|
|
|
+ d = fetch('uptime')
|
|
|
|
+ logging.debug('Loading uptime document done.')
|
|
|
|
+
|
|
|
|
+ if not 'relays' in d: raise Exception("No relays found in document.")
|
|
|
|
+ for r in d['relays']: self._add_uptime(r)
|
|
|
|
+
|
|
|
|
+ def add_relays(self):
|
|
|
|
+ self._add_details()
|
|
|
|
+ self._add_uptimes()
|
|
|
|
+
|
|
|
|
+ def count_guards(self):
|
|
|
|
+ guard_count = 0
|
|
|
|
+ for fpr in self.keys():
|
|
|
|
+ if self[fpr].is_guard():
|
|
|
|
+ guard_count += 1
|
|
|
|
+ return guard_count
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def compute_fallbacks(self):
|
|
|
|
+ self.fallbacks = map(lambda x: self[x],
|
|
|
|
+ sorted(
|
|
|
|
+ filter(lambda x: self[x].is_candidate(),
|
|
|
|
+ self.keys()),
|
|
|
|
+ key=lambda x: self[x]._data['consensus_weight'],
|
|
|
|
+ reverse=True)
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def load_relaylist(file_name):
|
|
|
|
+ """ Read each line in the file, and parse it like a FallbackDir line:
|
|
|
|
+ an IPv4 address and optional port:
|
|
|
|
+ <IPv4 address>:<port>
|
|
|
|
+ which are parsed into dictionary entries:
|
|
|
|
+ ipv4=<IPv4 address>
|
|
|
|
+ dirport=<port>
|
|
|
|
+ followed by a series of key=value entries:
|
|
|
|
+ orport=<port>
|
|
|
|
+ id=<fingerprint>
|
|
|
|
+ ipv6=<IPv6 address>:<IPv6 orport>
|
|
|
|
+ each line's key/value pairs are placed in a dictonary,
|
|
|
|
+ (of string -> string key/value pairs),
|
|
|
|
+ and these dictionaries are placed in an array.
|
|
|
|
+ comments start with
|
|
|
|
+ relaylist = []
|
|
|
|
+ file_data = read_from_file(file_name, MAX_LIST_FILE_SIZE)
|
|
|
|
+ if file_data is None:
|
|
|
|
+ return relaylist
|
|
|
|
+ for line in file_data.split('\n'):
|
|
|
|
+ relay_entry = {}
|
|
|
|
+
|
|
|
|
+ line_comment_split = line.split('#')
|
|
|
|
+ line = line_comment_split[0]
|
|
|
|
+
|
|
|
|
+ line = cleanse_whitespace(line)
|
|
|
|
+ line = line.strip()
|
|
|
|
+ if len(line) == 0:
|
|
|
|
+ continue
|
|
|
|
+ for item in line.split(' '):
|
|
|
|
+ item = item.strip()
|
|
|
|
+ if len(item) == 0:
|
|
|
|
+ continue
|
|
|
|
+ key_value_split = item.split('=')
|
|
|
|
+ kvl = len(key_value_split)
|
|
|
|
+ if kvl < 1 or kvl > 2:
|
|
|
|
+ print '#error Bad %s item: %s, format is key=value.'%(
|
|
|
|
+ file_name, item)
|
|
|
|
+ if kvl == 1:
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ ipv4_maybe_dirport = key_value_split[0]
|
|
|
|
+ ipv4_maybe_dirport_split = ipv4_maybe_dirport.split(':')
|
|
|
|
+ dirl = len(ipv4_maybe_dirport_split)
|
|
|
|
+ if dirl < 1 or dirl > 2:
|
|
|
|
+ print '#error Bad %s IPv4 item: %s, format is ipv4:port.'%(
|
|
|
|
+ file_name, item)
|
|
|
|
+ if dirl >= 1:
|
|
|
|
+ relay_entry['ipv4'] = ipv4_maybe_dirport_split[0]
|
|
|
|
+ if dirl == 2:
|
|
|
|
+ relay_entry['dirport'] = ipv4_maybe_dirport_split[1]
|
|
|
|
+ elif kvl == 2:
|
|
|
|
+ relay_entry[key_value_split[0]] = key_value_split[1]
|
|
|
|
+ relaylist.append(relay_entry)
|
|
|
|
+ return relaylist
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def apply_filter_lists(self):
|
|
|
|
+ excluded_count = 0
|
|
|
|
+ logging.debug('Applying whitelist and blacklist.')
|
|
|
|
+
|
|
|
|
+ whitelist = self.load_relaylist(WHITELIST_FILE_NAME)
|
|
|
|
+ blacklist = self.load_relaylist(BLACKLIST_FILE_NAME)
|
|
|
|
+ filtered_fallbacks = []
|
|
|
|
+ for f in self.fallbacks:
|
|
|
|
+ in_whitelist = f.is_in_whitelist(whitelist)
|
|
|
|
+ in_blacklist = f.is_in_blacklist(blacklist)
|
|
|
|
+ if in_whitelist and in_blacklist:
|
|
|
|
+ if BLACKLIST_EXCLUDES_WHITELIST_ENTRIES:
|
|
|
|
+
|
|
|
|
+ excluded_count += 1
|
|
|
|
+ logging.debug('Excluding %s: in both blacklist and whitelist.' %
|
|
|
|
+ f._fpr)
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ filtered_fallbacks.append(f)
|
|
|
|
+ elif in_whitelist:
|
|
|
|
+
|
|
|
|
+ filtered_fallbacks.append(f)
|
|
|
|
+ elif in_blacklist:
|
|
|
|
+
|
|
|
|
+ excluded_count += 1
|
|
|
|
+ logging.debug('Excluding %s: in blacklist.' %
|
|
|
|
+ f._fpr)
|
|
|
|
+ else:
|
|
|
|
+ if INCLUDE_UNLISTED_ENTRIES:
|
|
|
|
+
|
|
|
|
+ filtered_fallbacks.append(f)
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ excluded_count += 1
|
|
|
|
+ logging.debug('Excluding %s: in neither blacklist nor whitelist.' %
|
|
|
|
+ f._fpr)
|
|
|
|
+ self.fallbacks = filtered_fallbacks
|
|
|
|
+ return excluded_count
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def summarise_filters(initial_count, excluded_count):
|
|
|
|
+ return '/* Whitelist & blacklist excluded %d of %d candidates. */'%(
|
|
|
|
+ excluded_count, initial_count)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def exclude_excess_fallbacks(self):
|
|
|
|
+ self.fallbacks = self.fallbacks[:MAX_FALLBACK_COUNT]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def clamp_high_weight_fallbacks(self, total_weight):
|
|
|
|
+ if MAX_WEIGHT_FRACTION * len(self.fallbacks) < 1.0:
|
|
|
|
+ error_str = 'Max Fallback Weight %.3f%% is unachievable'%(
|
|
|
|
+ MAX_WEIGHT_FRACTION)
|
|
|
|
+ error_str += ' with Current Fallback Count %d.'%(len(self.fallbacks))
|
|
|
|
+ if STRICT_FALLBACK_WEIGHTS:
|
|
|
|
+ print '#error ' + error_str
|
|
|
|
+ else:
|
|
|
|
+ print '/* ' + error_str + ' */'
|
|
|
|
+ relays_clamped = 0
|
|
|
|
+ max_acceptable_weight = total_weight * MAX_WEIGHT_FRACTION
|
|
|
|
+ for f in self.fallbacks:
|
|
|
|
+ frac_weight = f.fallback_weight_fraction(total_weight)
|
|
|
|
+ if frac_weight > MAX_WEIGHT_FRACTION:
|
|
|
|
+ relays_clamped += 1
|
|
|
|
+ current_weight = f._data['consensus_weight']
|
|
|
|
+
|
|
|
|
+ if (not f._data.has_key('original_consensus_weight')
|
|
|
|
+ or f._data['original_consensus_weight'] == current_weight):
|
|
|
|
+ f._data['original_consensus_weight'] = current_weight
|
|
|
|
+ f._data['consensus_weight'] = max_acceptable_weight
|
|
|
|
+ return relays_clamped
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def exclude_low_weight_fallbacks(self, total_weight):
|
|
|
|
+ self.fallbacks = filter(
|
|
|
|
+ lambda x:
|
|
|
|
+ x.fallback_weight_fraction(total_weight) >= MIN_WEIGHT_FRACTION,
|
|
|
|
+ self.fallbacks)
|
|
|
|
+
|
|
|
|
+ def fallback_weight_total(self):
|
|
|
|
+ return sum(f._data['consensus_weight'] for f in self.fallbacks)
|
|
|
|
+
|
|
|
|
+ def fallback_min_weight(self):
|
|
|
|
+ if len(self.fallbacks) > 0:
|
|
|
|
+ return self.fallbacks[-1]
|
|
|
|
+ else:
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def fallback_max_weight(self):
|
|
|
|
+ if len(self.fallbacks) > 0:
|
|
|
|
+ return self.fallbacks[0]
|
|
|
|
+ else:
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def summarise_fallbacks(self, eligible_count, eligible_weight,
|
|
|
|
+ relays_clamped, clamped_weight,
|
|
|
|
+ guard_count, target_count, max_count):
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ s = '/*'
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += 'Fallback Directory Summary'
|
|
|
|
+ s += '\n'
|
|
|
|
+
|
|
|
|
+ fallback_count = len(self.fallbacks)
|
|
|
|
+ if FALLBACK_PROPORTION_OF_GUARDS is None:
|
|
|
|
+ fallback_proportion = ''
|
|
|
|
+ else:
|
|
|
|
+ fallback_proportion = ' (%d * %f)'%(guard_count,
|
|
|
|
+ FALLBACK_PROPORTION_OF_GUARDS)
|
|
|
|
+ s += 'Final Count: %d (Eligible %d, Usable %d, Target %d%s, '%(
|
|
|
|
+ min(max_count, fallback_count),
|
|
|
|
+ eligible_count,
|
|
|
|
+ fallback_count,
|
|
|
|
+ target_count,
|
|
|
|
+ fallback_proportion)
|
|
|
|
+ s += 'Clamped to %d)'%(
|
|
|
|
+ MAX_FALLBACK_COUNT)
|
|
|
|
+ s += '\n'
|
|
|
|
+ if fallback_count < MIN_FALLBACK_COUNT:
|
|
|
|
+ s += '*/'
|
|
|
|
+ s += '\n'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ s += '#error Fallback Count %d is too low. '%(fallback_count)
|
|
|
|
+ s += 'Must be at least %d for diversity. '%(MIN_FALLBACK_COUNT)
|
|
|
|
+ s += 'Try adding entries to the whitelist, '
|
|
|
|
+ s += 'or setting INCLUDE_UNLISTED_ENTRIES = True.'
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += '/*'
|
|
|
|
+ s += '\n'
|
|
|
|
+ total_weight = self.fallback_weight_total()
|
|
|
|
+ min_fb = self.fallback_min_weight()
|
|
|
|
+ min_weight = min_fb._data['consensus_weight']
|
|
|
|
+ min_percent = min_fb.fallback_weight_fraction(total_weight)*100.0
|
|
|
|
+ max_fb = self.fallback_max_weight()
|
|
|
|
+ max_weight = max_fb._data['consensus_weight']
|
|
|
|
+ max_frac = max_fb.fallback_weight_fraction(total_weight)
|
|
|
|
+ max_percent = max_frac*100.0
|
|
|
|
+ s += 'Final Weight: %d (Eligible %d)'%(total_weight, eligible_weight)
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += 'Max Weight: %d (%.3f%%) (Clamped to %.3f%%)'%(
|
|
|
|
+ max_weight,
|
|
|
|
+ max_percent,
|
|
|
|
+ TARGET_MAX_WEIGHT_FRACTION*100)
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += 'Min Weight: %d (%.3f%%) (Clamped to %.3f%%)'%(
|
|
|
|
+ min_weight,
|
|
|
|
+ min_percent,
|
|
|
|
+ MIN_WEIGHT_FRACTION*100)
|
|
|
|
+ s += '\n'
|
|
|
|
+ if eligible_count != fallback_count:
|
|
|
|
+ s += 'Excluded: %d (Clamped, Below Target, or Low Weight)'%(
|
|
|
|
+ eligible_count - fallback_count)
|
|
|
|
+ s += '\n'
|
|
|
|
+ if relays_clamped > 0:
|
|
|
|
+ s += 'Clamped: %d (%.3f%%) Excess Weight, '%(
|
|
|
|
+ clamped_weight,
|
|
|
|
+ (100.0 * clamped_weight) / total_weight)
|
|
|
|
+ s += '%d High Weight Fallbacks (%.1f%%)'%(
|
|
|
|
+ relays_clamped,
|
|
|
|
+ (100.0 * relays_clamped) / fallback_count)
|
|
|
|
+ s += '\n'
|
|
|
|
+ s += '*/'
|
|
|
|
+ if max_frac > TARGET_MAX_WEIGHT_FRACTION:
|
|
|
|
+ s += '\n'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ error_str = 'Max Fallback Weight %.3f%% is too high. '%(max_frac*100)
|
|
|
|
+ error_str += 'Must be at most %.3f%% for client anonymity.'%(
|
|
|
|
+ TARGET_MAX_WEIGHT_FRACTION*100)
|
|
|
|
+ if STRICT_FALLBACK_WEIGHTS:
|
|
|
|
+ s += '#error ' + error_str
|
|
|
|
+ else:
|
|
|
|
+ s += '/* ' + error_str + ' */'
|
|
|
|
+ return s
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def list_fallbacks():
|
|
|
|
+ """ Fetches required onionoo documents and evaluates the
|
|
|
|
+ fallback directory criteria for each of the relays """
|
|
|
|
+
|
|
|
|
+ candidates = CandidateList()
|
|
|
|
+ candidates.add_relays()
|
|
|
|
+
|
|
|
|
+ guard_count = candidates.count_guards()
|
|
|
|
+ if FALLBACK_PROPORTION_OF_GUARDS is None:
|
|
|
|
+ target_count = MAX_FALLBACK_COUNT
|
|
|
|
+ else:
|
|
|
|
+ target_count = int(guard_count * FALLBACK_PROPORTION_OF_GUARDS)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ max_count = min(target_count, MAX_FALLBACK_COUNT)
|
|
|
|
+
|
|
|
|
+ candidates.compute_fallbacks()
|
|
|
|
+
|
|
|
|
+ initial_count = len(candidates.fallbacks)
|
|
|
|
+ excluded_count = candidates.apply_filter_lists()
|
|
|
|
+ print candidates.summarise_filters(initial_count, excluded_count)
|
|
|
|
+
|
|
|
|
+ eligible_count = len(candidates.fallbacks)
|
|
|
|
+ eligible_weight = candidates.fallback_weight_total()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ candidates.exclude_excess_fallbacks()
|
|
|
|
+ total_weight = candidates.fallback_weight_total()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ pre_clamp_total_weight = total_weight
|
|
|
|
+ relays_clamped = candidates.clamp_high_weight_fallbacks(total_weight)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ candidates.exclude_low_weight_fallbacks(total_weight)
|
|
|
|
+ total_weight = candidates.fallback_weight_total()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if len(candidates.fallbacks) > 0:
|
|
|
|
+ max_weight_fb = candidates.fallback_max_weight()
|
|
|
|
+ max_weight = max_weight_fb.fallback_weight_fraction(total_weight)
|
|
|
|
+ if max_weight > TARGET_MAX_WEIGHT_FRACTION:
|
|
|
|
+ error_str = 'Maximum fallback weight: %.3f%% exceeds target %.3f%%. '%(
|
|
|
|
+ max_weight,
|
|
|
|
+ TARGET_MAX_WEIGHT_FRACTION)
|
|
|
|
+ error_str += 'Try decreasing REWEIGHTING_FUDGE_FACTOR.'
|
|
|
|
+ if STRICT_FALLBACK_WEIGHTS:
|
|
|
|
+ print '#error ' + error_str
|
|
|
|
+ else:
|
|
|
|
+ print '/* ' + error_str + ' */'
|
|
|
|
+
|
|
|
|
+ print candidates.summarise_fallbacks(eligible_count, eligible_weight,
|
|
|
|
+ relays_clamped,
|
|
|
|
+ pre_clamp_total_weight - total_weight,
|
|
|
|
+ guard_count, target_count, max_count)
|
|
|
|
+ else:
|
|
|
|
+ print '/* No Fallbacks met criteria */'
|
|
|
|
+
|
|
|
|
+ for s in fetch_source_list():
|
|
|
|
+ print describe_fetch_source(s)
|
|
|
|
+
|
|
|
|
+ for x in candidates.fallbacks[:max_count]:
|
|
|
|
+ print x.fallbackdir_line(total_weight, pre_clamp_total_weight)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
+ list_fallbacks()
|