Skip to content
Permalink
bug_6696
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
executable file 408 lines (382 sloc) 18.8 KB
#!/usr/bin/env python
#
# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# http://sam.zoy.org/wtfpl/COPYING for more details.
import json
import operator
import sys
import os
from optparse import OptionParser, OptionGroup
import urllib
import re
from abc import abstractmethod
class BaseFilter(object):
@abstractmethod
def accept(self, relay):
pass
def load(self, relays):
return filter(self.accept, relays)
class RunningFilter(BaseFilter):
def accept(self, relay):
return relay['running']
class FamilyFilter(BaseFilter):
def __init__(self, family, all_relays):
self._family_fingerprint = None
self._family_nickname = None
self._family_relays = []
found_relay = None
for relay in all_relays:
if len(family) == 40 and relay['fingerprint'] == family:
found_relay = relay
break
if len(family) < 20 and 'Named' in relay['flags'] and relay['nickname'] == family:
found_relay = relay
break
if found_relay:
self._family_fingerprint = '$%s' % found_relay['fingerprint']
if 'Named' in found_relay['flags']:
self._family_nickname = found_relay['nickname']
self._family_relays = [self._family_fingerprint] + found_relay.get('family', [])
def accept(self, relay):
fingerprint = '$%s' % relay['fingerprint']
mentions = [fingerprint] + relay.get('family', [])
# Only show families as accepted by consensus (mutually listed relays)
listed = fingerprint in self._family_relays
listed = listed or 'Named' in relay['flags'] and relay['nickname'] in self._family_relays
mentioned = self._family_fingerprint in mentions
mentioned = mentioned or self._family_nickname in mentions
if listed and mentioned:
return True
return False
class CountryFilter(BaseFilter):
def __init__(self, countries=[]):
self._countries = [x.lower() for x in countries]
def accept(self, relay):
return relay.get('country', None) in self._countries
class ASFilter(BaseFilter):
def __init__(self, as_sets=[]):
self._as_sets = [x if not x.isdigit() else "AS" + x for x in as_sets]
def accept(self, relay):
return relay.get('as_number', None) in self._as_sets
class ExitFilter(BaseFilter):
def accept(self, relay):
return relay.get('exit_probability', -1) > 0.0
class GuardFilter(BaseFilter):
def accept(self, relay):
return relay.get('guard_probability', -1) > 0.0
class FastExitFilter(BaseFilter):
class Relay(object):
def __init__(self, relay):
self.exit = relay.get('exit_probability')
self.fp = relay.get('fingerprint')
self.relay = relay
def __init__(self, bandwidth_rate, advertised_bandwidth, ports, same_network, inverse=False):
self.bandwidth_rate = bandwidth_rate
self.advertised_bandwidth = advertised_bandwidth
self.ports = ports
self.same_network = same_network
self.inverse = inverse
def load(self, all_relays):
# First, filter relays based on bandwidth and port requirements.
matching_relays = []
for relay in all_relays:
if relay.get('bandwidth_rate', -1) < self.bandwidth_rate:
continue
if relay.get('advertised_bandwidth', -1) < self.advertised_bandwidth:
continue
relevant_ports = set(self.ports)
summary = relay.get('exit_policy_summary', {})
if 'accept' in summary:
portlist = summary['accept']
elif 'reject' in summary:
portlist = summary['reject']
else:
continue
ports = []
for p in portlist:
if '-' in p:
ports.extend(range(int(p.split('-')[0]),
int(p.split('-')[1]) + 1))
else:
ports.append(int(p))
policy_ports = set(ports)
if 'accept' in summary and not relevant_ports.issubset(policy_ports):
continue
if 'reject' in summary and not relevant_ports.isdisjoint(policy_ports):
continue
matching_relays.append(relay)
# Second, filter relays based on same /24 requirement.
if self.same_network:
network_data = {}
for relay in matching_relays:
or_addresses = relay.get("or_addresses")
no_of_addresses = 0
for ip in or_addresses:
ip, port = ip.rsplit(':', 1)
# skip if ipv6
if ':' in ip:
continue
no_of_addresses += 1
if no_of_addresses > 1:
print "[WARNING] - %s has more than one IPv4 OR address - %s" % relay.get("fingerprint"), or_addresses
network = ip.rsplit('.', 1)[0]
relay_info = self.Relay(relay)
if network_data.has_key(network):
if len(network_data[network]) > 1:
# assume current relay to have smallest exit_probability
min_exit = relay.get('exit_probability')
min_id = -1
for id, value in enumerate(network_data[network]):
if value.exit < min_exit:
min_exit = value.exit
min_id = id
if min_id != -1:
del network_data[network][min_id]
network_data[network].append(relay_info)
else:
network_data[network].append(relay_info)
else:
network_data[network] = [relay_info]
matching_relays = []
for relay_list in network_data.values():
matching_relays.extend([relay.relay for relay in relay_list])
# Either return relays meeting all requirements, or the inverse set.
if self.inverse:
inverse_relays = []
for relay in all_relays:
if relay not in matching_relays:
inverse_relays.append(relay)
return inverse_relays
else:
return matching_relays
class RelayStats(object):
def __init__(self, options):
self._data = None
self._filters = self._create_filters(options)
self._get_group = self._get_group_function(options)
self._relays = None
@property
def data(self):
if not self._data:
self._data = json.load(file(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'details.json')))
return self._data
@property
def relays(self):
if self._relays:
return self._relays
self._relays = {}
relays = self.data['relays']
for f in self._filters:
relays = f.load(relays)
for relay in relays:
self.add_relay(relay)
return self._relays
def _create_filters(self, options):
filters = []
if not options.inactive:
filters.append(RunningFilter())
if options.family:
filters.append(FamilyFilter(options.family, self.data['relays']))
if options.country:
filters.append(CountryFilter(options.country))
if options.ases:
filters.append(ASFilter(options.ases))
if options.exits_only:
filters.append(ExitFilter())
if options.guards_only:
filters.append(GuardFilter())
if options.fast_exits_only:
filters.append(FastExitFilter(95 * 125 * 1024, 5000 * 1024, [80, 443, 554, 1755], True, False))
if options.almost_fast_exits_only:
filters.append(FastExitFilter(80 * 125 * 1024, 2000 * 1024, [80, 443], False, False))
filters.append(FastExitFilter(95 * 125 * 1024, 5000 * 1024, [80, 443, 554, 1755], True, True))
if options.fast_exits_only_any_network:
filters.append(FastExitFilter(95 * 125 * 1024, 5000 * 1024, [80, 443, 554, 1755], False, False))
return filters
def _get_group_function(self, options):
if options.by_country and options.by_as:
return lambda relay: (relay.get('country', None), relay.get('as_number', None))
elif options.by_country:
return lambda relay: relay.get('country', None)
elif options.by_as:
return lambda relay: relay.get('as_number', None)
else:
return lambda relay: relay.get('fingerprint')
def add_relay(self, relay):
key = self._get_group(relay)
if key not in self._relays:
self._relays[key] = []
self._relays[key].append(relay)
def format_and_sort_groups(self, grouped_relays, country=None, ases=None, by_country=False, by_as_number=False, links=False):
formatted_groups = {}
for group in grouped_relays.values():
group_weights = (0, 0, 0, 0, 0, 0)
relays_in_group, exits_in_group, guards_in_group = 0, 0, 0
ases_in_group = set()
for relay in group:
weights = (relay.get('consensus_weight_fraction', 0),
relay.get('advertised_bandwidth', 0),
relay.get('advertised_bandwidth_fraction', 0),
relay.get('guard_probability', 0),
relay.get('middle_probability', 0),
relay.get('exit_probability', 0))
group_weights = tuple(sum(x) for x in zip(group_weights, weights))
nickname = relay['nickname']
fingerprint = relay['fingerprint'] if not links else "https://atlas.torproject.org/#details/%s" % relay['fingerprint']
if 'Exit' in set(relay['flags']):
exit = 'Exit'
exits_in_group += 1
else:
exit = '-'
if 'Guard' in set(relay['flags']):
guard = 'Guard'
guards_in_group += 1
else:
guard = '-'
country = relay.get('country', '??')
as_number = relay.get('as_number', '??')
as_name = relay.get('as_name', '??')
as_info = "%s %s" %(as_number, as_name)
ases_in_group.add(as_info)
relays_in_group += 1
if by_country or by_as_number:
nickname = "*"
fingerprint = "(%d relays)" % relays_in_group
exit = "(%d)" % exits_in_group
guard = "(%d)" % guards_in_group
if not by_as_number and not ases:
as_info = "(%s)" % len(ases_in_group)
if not by_country and not country:
country = "*"
if links:
format_string = "%8.4f%% %1.2f %4.2f%% %8.4f%% %8.4f%% %8.4f%% %-19s %-78s %-4s %-5s %-2s %-9s"
else:
format_string = "%8.4f%% %1.2f %4.2f%% %8.4f%% %8.4f%% %8.4f%% %-19s %-40s %-4s %-5s %-2s %-9s"
formatted_group = format_string % (
group_weights[0] * 100.0,
group_weights[1] / (1024.0 * 1024.0),
group_weights[2] * 100.0,
group_weights[3] * 100.0,
group_weights[4] * 100.0,
group_weights[5] * 100.0,
nickname, fingerprint,
exit, guard, country, as_info)
formatted_groups[formatted_group] = group_weights
sorted_groups = sorted(formatted_groups.iteritems(), key=operator.itemgetter(1))
sorted_groups.reverse()
return sorted_groups
def print_groups(self, sorted_groups, count=10, by_country=False, by_as_number=False, short=False, links=False):
output_string = []
if links:
output_string.append(" CW adv_bw(MB/s) P_guard P_Middle P_exit Nickname Link Exit Guard CC Autonomous System"[:short])
else:
output_string.append(" CW adv_bw(MB/s) P_guard P_Middle P_exit Nickname Fingerprint Exit Guard CC Autonomous System"[:short])
if count < 0: count = len(sorted_groups)
for formatted_group, weight in sorted_groups[:count]:
output_string.append(formatted_group[:short])
if len(sorted_groups) > count:
if by_country and by_as_number:
type = "countries and ASes"
elif by_country:
type = "countries"
elif by_as_number:
type = "ASes"
else:
type = "relays"
other_weights = (0, 0, 0, 0, 0, 0)
for _, weights in sorted_groups[count:]:
other_weights = tuple(sum(x) for x in zip(other_weights, weights))
output_string.append("%8.4f%% %4.2f%% %8.4f%% %8.4f%% %8.4f%% (%d other %s)" % (
other_weights[0] * 100.0, other_weights[2] * 100.0,
other_weights[3] * 100.0, other_weights[4] * 100.0,
other_weights[5] * 100.0, len(sorted_groups) - count, type))
selection_weights = (0, 0, 0, 0, 0)
for _, weights in sorted_groups:
selection_weights = tuple(sum(x) for x in zip(selection_weights, weights))
if len(sorted_groups) > 1 and selection_weights[0] < 0.999:
output_string.append("%8.4f%% %4.2f%% %8.4f%% %8.4f%% %8.4f%% (total in selection)" % (
selection_weights[0] * 100.0, selection_weights[2] * 100.0,
selection_weights[3] * 100.0, selection_weights[4] * 100.0,
selection_weights[5] * 100.0))
return output_string
def create_option_parser():
parser = OptionParser()
parser.add_option("-d", "--download", action="store_true",
help="download details.json from Onionoo service")
group = OptionGroup(parser, "Filtering options")
group.add_option("-i", "--inactive", action="store_true", default=False,
help="include relays in selection that aren't currently running")
group.add_option("-a", "--as", dest="ases", action="append",
help="select only relays from autonomous system number AS",
metavar="AS")
group.add_option("-c", "--country", action="append",
help="select only relays from country with code CC", metavar="CC")
group.add_option("-e", "--exits-only", action="store_true",
help="select only relays suitable for exit position")
group.add_option("-f", "--family", action="store", type="string", metavar="RELAY",
help="select family by fingerprint or nickname (for named relays)")
group.add_option("-g", "--guards-only", action="store_true",
help="select only relays suitable for guard position")
group.add_option("--fast-exits-only", action="store_true",
help="select only fast exits (95+ Mbit/s, 5000+ KB/s, 80/443/554/1755, 2- per /24)")
group.add_option("--almost-fast-exits-only", action="store_true",
help="select only almost fast exits (80+ Mbit/s, 2000+ KB/s, 80/443, not in set of fast exits)")
group.add_option("--fast-exits-only-any-network", action="store_true",
help="select only fast exits without network restriction (95+ Mbit/s, 5000+ KB/s, 80/443/554/1755")
parser.add_option_group(group)
group = OptionGroup(parser, "Grouping options")
group.add_option("-A", "--by-as", action="store_true", default=False,
help="group relays by AS")
group.add_option("-C", "--by-country", action="store_true", default=False,
help="group relays by country")
parser.add_option_group(group)
group = OptionGroup(parser, "Display options")
group.add_option("-l", "--links", action="store_true",
help="display links to the Atlas service instead of fingerprints")
group.add_option("-t", "--top", type="int", default=10, metavar="NUM",
help="display only the top results (default: %default; -1 for all)")
group.add_option("-s", "--short", action="store_true",
help="cut the length of the line output at 70 chars")
parser.add_option_group(group)
return parser
def download_details_file():
url = urllib.urlopen('https://onionoo.torproject.org/details?type=relay')
details_file = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'details.json'), 'w')
details_file.write(url.read())
url.close()
details_file.close()
if '__main__' == __name__:
parser = create_option_parser()
(options, args) = parser.parse_args()
if len(args) > 0:
parser.error("Did not understand positional argument(s), use options instead.")
if options.family and not re.match(r'^[A-F0-9]{40}$', options.family) and not re.match(r'^[A-Za-z0-9]{1,19}$', options.family):
parser.error("Not a valid fingerprint or nickname: %s" % options.family)
fast_exit_options = 0
if options.fast_exits_only: fast_exit_options += 1
if options.almost_fast_exits_only: fast_exit_options += 1
if options.fast_exits_only_any_network: fast_exit_options += 1
if fast_exit_options > 1:
parser.error("Can only filter by one fast-exit option.")
if options.download:
download_details_file()
print "Downloaded details.json. Re-run without --download option."
exit()
if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'details.json')):
parser.error("Did not find details.json. Re-run with --download.")
stats = RelayStats(options)
sorted_groups = stats.format_and_sort_groups(stats.relays,
country=options.country,
ases=options.ases,
by_country=options.by_country,
by_as_number=options.by_as,
links=options.links)
output_string = stats.print_groups(sorted_groups, options.top,
by_country=options.by_country,
by_as_number=options.by_as,
short=70 if options.short else None,
links=options.links)
print '\n'.join(output_string)