Replace tpo git repository URL by gitlab
[stem.git] / stem / directory.py
blob710e598eda1cb706427254879f71f4702b504da7
1 # Copyright 2018-2020, Damian Johnson and The Tor Project
2 # See LICENSE for licensing information
4 """
5 Directories that provide `relay descriptor information
6 <../tutorials/mirror_mirror_on_the_wall.html>`_. At a very high level tor works
7 as follows...
9 1. Volunteer starts a new tor relay, during which it sends a `server
10 descriptor <descriptor/server_descriptor.html>`_ to each of the directory
11 authorities.
13 2. Each hour the directory authorities make a `vote
14 <descriptor/networkstatus.html>`_ that says who they think the active
15 relays are in the network and some attributes about them.
17 3. The directory authorities send each other their votes, and compile that
18 into the `consensus <descriptor/networkstatus.html>`_. This document is very
19 similar to the votes, the only difference being that the majority of the
20 authorities agree upon and sign this document. The idividual relay entries
21 in the vote or consensus is called `router status entries
22 <descriptor/router_status_entry.html>`_.
24 4. Tor clients (people using the service) download the consensus from an
25 authority, fallback, or other mirror to determine who the active relays in
26 the network are. They then use this to construct circuits and use the
27 network.
31 Directory - Relay we can retrieve descriptor information from
32 | |- from_cache - Provides cached information bundled with Stem.
33 | +- from_remote - Downloads the latest directory information from tor.
35 |- Authority - Tor directory authority
36 +- Fallback - Mirrors that can be used instead of the authorities
38 .. versionadded:: 1.7.0
39 """
41 import collections
42 import os
43 import re
44 import sys
45 import urllib.request
47 import stem
48 import stem.util
49 import stem.util.conf
51 from stem.util import connection, str_tools, tor_tools
52 from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Pattern, Sequence, Tuple, Union
54 GITWEB_AUTHORITY_URL = 'https://gitweb.torproject.org/tor.git/plain/src/app/config/auth_dirs.inc'
55 GITWEB_FALLBACK_URL = 'https://gitweb.torproject.org/tor.git/plain/src/app/config/fallback_dirs.inc'
56 FALLBACK_CACHE_PATH = os.path.join(os.path.dirname(__file__), 'cached_fallbacks.cfg')
58 AUTHORITY_NAME = re.compile('"(\\S+) orport=(\\d+) .*"')
59 AUTHORITY_V3IDENT = re.compile('"v3ident=([\\dA-F]{40}) "')
60 AUTHORITY_IPV6 = re.compile('"ipv6=\\[([\\da-f:]+)\\]:(\\d+) "')
61 AUTHORITY_ADDR = re.compile('"([\\d\\.]+):(\\d+) ([\\dA-F ]{49})",')
63 FALLBACK_DIV = '/* ===== */'
64 FALLBACK_MAPPING = re.compile('/\\*\\s+(\\S+)=(\\S*)\\s+\\*/')
66 FALLBACK_ADDR = re.compile('"([\\d\\.]+):(\\d+) orport=(\\d+) id=([\\dA-F]{40}).*')
67 FALLBACK_NICKNAME = re.compile('/\\* nickname=(\\S+) \\*/')
68 FALLBACK_EXTRAINFO = re.compile('/\\* extrainfo=([0-1]) \\*/')
69 FALLBACK_IPV6 = re.compile('" ipv6=\\[([\\da-f:]+)\\]:(\\d+)"')
72 def _match_with(lines: Sequence[str], regexes: Sequence[Pattern], required: Optional[Sequence[Pattern]] = None) -> Dict[Pattern, Union[str, List[str]]]:
73 """
74 Scans the given content against a series of regex matchers, providing back a
75 mapping of regexes to their capture groups. This maping is with the value if
76 the regex has just a single capture group, and a tuple otherwise.
78 :param lines: text to parse
79 :param regexes: regexes to match against
80 :param required: matches that must be in the content
82 :returns: **dict** mapping matchers against their capture groups
84 :raises: **ValueError** if a required match is not present
85 """
87 matches = {}
89 for line in lines:
90 for matcher in regexes:
91 m = matcher.search(str_tools._to_unicode(line))
93 if m:
94 match_groups = m.groups()
95 matches[matcher] = match_groups if len(match_groups) > 1 else match_groups[0]
97 if required:
98 for required_matcher in required:
99 if required_matcher not in matches:
100 raise ValueError('Failed to parse mandatory data from:\n\n%s' % '\n'.join(lines))
102 return matches
105 def _directory_entries(lines: List[str], pop_section_func: Callable[[List[str]], List[str]], regexes: Sequence[Pattern], required: Optional[Sequence[Pattern]] = None) -> Iterator[Dict[Pattern, Union[str, List[str]]]]:
106 next_section = pop_section_func(lines)
108 while next_section:
109 yield _match_with(next_section, regexes, required)
110 next_section = pop_section_func(lines)
113 class Directory(object):
115 Relay we can contact for descriptor information.
117 Our :func:`~stem.directory.Directory.from_cache` and
118 :func:`~stem.directory.Directory.from_remote` functions key off a
119 different identifier based on our subclass...
121 * :class:`~stem.directory.Authority` keys off the nickname.
122 * :class:`~stem.directory.Fallback` keys off fingerprints.
124 This is because authorities are highly static and canonically known by their
125 names, whereas fallbacks vary more and don't necessarily have a nickname to
126 key off of.
128 :var str address: IPv4 address of the directory
129 :var int or_port: port on which the relay services relay traffic
130 :var int dir_port: port on which directory information is available
131 :var str fingerprint: relay fingerprint
132 :var str nickname: relay nickname
133 :var tuple orport_v6: **(address, port)** tuple for the directory's IPv6
134 ORPort, or **None** if it doesn't have one
137 def __init__(self, address: str, or_port: Union[int, str], dir_port: Union[int, str], fingerprint: str, nickname: str, orport_v6: Tuple[str, int]) -> None:
138 identifier = '%s (%s)' % (fingerprint, nickname) if nickname else fingerprint
140 if not connection.is_valid_ipv4_address(address):
141 raise ValueError('%s has an invalid IPv4 address: %s' % (identifier, address))
142 elif not connection.is_valid_port(or_port):
143 raise ValueError('%s has an invalid ORPort: %s' % (identifier, or_port))
144 elif not connection.is_valid_port(dir_port):
145 raise ValueError('%s has an invalid DirPort: %s' % (identifier, dir_port))
146 elif not tor_tools.is_valid_fingerprint(fingerprint):
147 raise ValueError('%s has an invalid fingerprint: %s' % (identifier, fingerprint))
148 elif nickname and not tor_tools.is_valid_nickname(nickname):
149 raise ValueError('%s has an invalid nickname: %s' % (fingerprint, nickname))
151 if orport_v6:
152 if not isinstance(orport_v6, tuple) or len(orport_v6) != 2:
153 raise ValueError('%s orport_v6 should be a two value tuple: %s' % (identifier, str(orport_v6)))
154 elif not connection.is_valid_ipv6_address(orport_v6[0]):
155 raise ValueError('%s has an invalid IPv6 address: %s' % (identifier, orport_v6[0]))
156 elif not connection.is_valid_port(orport_v6[1]):
157 raise ValueError('%s has an invalid IPv6 port: %s' % (identifier, orport_v6[1]))
159 self.address = address
160 self.or_port = int(or_port)
161 self.dir_port = int(dir_port)
162 self.fingerprint = fingerprint
163 self.nickname = nickname
164 self.orport_v6 = (orport_v6[0], int(orport_v6[1])) if orport_v6 else None
166 @staticmethod
167 def from_cache() -> Dict[str, Any]:
169 Provides cached Tor directory information. This information is hardcoded
170 into Tor and occasionally changes, so the information provided by this
171 method may not necessarily match the latest version of tor.
173 .. versionadded:: 1.5.0
175 .. versionchanged:: 1.7.0
176 Support added to the :class:`~stem.directory.Authority` class.
178 :returns: **dict** of **str** identifiers to
179 :class:`~stem.directory.Directory` instances
182 raise NotImplementedError('Unsupported Operation: this should be implemented by the Directory subclass')
184 @staticmethod
185 def from_remote(timeout: int = 60) -> Dict[str, Any]:
187 Reads and parses tor's directory data `from gitweb.torproject.org <https://gitweb.torproject.org/>`_.
188 Note that while convenient, this reliance on GitWeb means you should alway
189 call with a fallback, such as...
193 try:
194 authorities = stem.directory.Authority.from_remote()
195 except OSError:
196 authorities = stem.directory.Authority.from_cache()
198 .. versionadded:: 1.5.0
200 .. versionchanged:: 1.7.0
201 Support added to the :class:`~stem.directory.Authority` class.
203 :param timeout: seconds to wait before timing out the request
205 :returns: **dict** of **str** identifiers to their
206 :class:`~stem.directory.Directory`
208 :raises: **OSError** if unable to retrieve the fallback directories
211 raise NotImplementedError('Unsupported Operation: this should be implemented by the Directory subclass')
213 def __hash__(self) -> int:
214 return stem.util._hash_attr(self, 'address', 'or_port', 'dir_port', 'fingerprint', 'nickname', 'orport_v6')
216 def __eq__(self, other: Any) -> bool:
217 return hash(self) == hash(other) if isinstance(other, Directory) else False
219 def __ne__(self, other: Any) -> bool:
220 return not self == other
223 class Authority(Directory):
225 Tor directory authority, a special type of relay `hardcoded into tor
226 <https://gitweb.torproject.org/tor.git/plain/src/or/auth_dirs.inc>`_
227 to enumerate the relays in the network.
229 .. versionchanged:: 1.7.0
230 Added the orport_v6 attribute.
232 :var str v3ident: identity key fingerprint used to sign votes and consensus
235 def __init__(self, address: Optional[str] = None, or_port: Optional[Union[int, str]] = None, dir_port: Optional[Union[int, str]] = None, fingerprint: Optional[str] = None, nickname: Optional[str] = None, orport_v6: Optional[Tuple[str, int]] = None, v3ident: Optional[str] = None) -> None:
236 super(Authority, self).__init__(address, or_port, dir_port, fingerprint, nickname, orport_v6)
238 if v3ident and not tor_tools.is_valid_fingerprint(v3ident):
239 identifier = '%s (%s)' % (fingerprint, nickname) if nickname else fingerprint
240 raise ValueError('%s has an invalid v3ident: %s' % (identifier, v3ident))
242 self.v3ident = v3ident
244 @staticmethod
245 def from_cache() -> Dict[str, 'stem.directory.Authority']:
246 return dict(DIRECTORY_AUTHORITIES)
248 @staticmethod
249 def from_remote(timeout: int = 60) -> Dict[str, 'stem.directory.Authority']:
250 try:
251 lines = str_tools._to_unicode(urllib.request.urlopen(GITWEB_AUTHORITY_URL, timeout = timeout).read()).splitlines()
253 if not lines:
254 raise OSError('no content')
255 except:
256 exc, stacktrace = sys.exc_info()[1:3]
257 message = "Unable to download tor's directory authorities from %s: %s" % (GITWEB_AUTHORITY_URL, exc)
258 raise stem.DownloadFailed(GITWEB_AUTHORITY_URL, exc, stacktrace, message)
260 # Entries look like...
262 # "moria1 orport=9101 "
263 # "v3ident=D586D18309DED4CD6D57C18FDB97EFA96D330566 "
264 # "128.31.0.39:9131 9695 DFC3 5FFE B861 329B 9F1A B04C 4639 7020 CE31",
266 try:
267 results = {}
269 for matches in _directory_entries(lines, Authority._pop_section, (AUTHORITY_NAME, AUTHORITY_V3IDENT, AUTHORITY_IPV6, AUTHORITY_ADDR), required = (AUTHORITY_NAME, AUTHORITY_ADDR)):
270 nickname, or_port = matches.get(AUTHORITY_NAME) # type: ignore
271 address, dir_port, fingerprint = matches.get(AUTHORITY_ADDR) # type: ignore
273 results[nickname] = Authority(
274 address = address,
275 or_port = or_port,
276 dir_port = dir_port,
277 fingerprint = fingerprint.replace(' ', ''),
278 nickname = nickname,
279 orport_v6 = matches.get(AUTHORITY_IPV6), # type: ignore
280 v3ident = matches.get(AUTHORITY_V3IDENT), # type: ignore
282 except ValueError as exc:
283 raise OSError(str(exc))
285 return results
287 @staticmethod
288 def _pop_section(lines: List[str]) -> List[str]:
290 Provides the next authority entry.
293 section_lines = []
295 if lines:
296 section_lines.append(lines.pop(0))
298 while lines and lines[0].startswith(' '):
299 section_lines.append(lines.pop(0))
301 return section_lines
303 def __hash__(self) -> int:
304 return stem.util._hash_attr(self, 'v3ident', parent = Directory, cache = True)
306 def __eq__(self, other: Any) -> bool:
307 return hash(self) == hash(other) if isinstance(other, Authority) else False
309 def __ne__(self, other: Any) -> bool:
310 return not self == other
313 class Fallback(Directory):
315 Particularly stable relays tor can instead of authorities when
316 bootstrapping. These relays are `hardcoded in tor
317 <https://gitweb.torproject.org/tor.git/tree/src/or/fallback_dirs.inc>`_.
319 For example, the following checks the performance of tor's fallback directories...
323 import time
324 from stem.descriptor.remote import get_consensus
325 from stem.directory import Fallback
327 for fallback in Fallback.from_cache().values():
328 start = time.time()
329 get_consensus(endpoints = [(fallback.address, fallback.dir_port)]).run()
330 print('Downloading the consensus took %0.2f from %s' % (time.time() - start, fallback.fingerprint))
334 % python example.py
335 Downloading the consensus took 5.07 from 0AD3FA884D18F89EEA2D89C019379E0E7FD94417
336 Downloading the consensus took 3.59 from C871C91489886D5E2E94C13EA1A5FDC4B6DC5204
337 Downloading the consensus took 4.16 from 74A910646BCEEFBCD2E874FC1DC997430F968145
340 .. versionadded:: 1.5.0
342 .. versionchanged:: 1.7.0
343 Added the has_extrainfo and header attributes which are part of
344 the `second version of the fallback directories
345 <https://lists.torproject.org/pipermail/tor-dev/2017-December/012721.html>`_.
347 :var bool has_extrainfo: **True** if the relay should be able to provide
348 extrainfo descriptors, **False** otherwise.
349 :var collections.OrderedDict header: metadata about the fallback directory file this originated from
352 def __init__(self, address: Optional[str] = None, or_port: Optional[Union[int, str]] = None, dir_port: Optional[Union[int, str]] = None, fingerprint: Optional[str] = None, nickname: Optional[str] = None, has_extrainfo: bool = False, orport_v6: Optional[Tuple[str, int]] = None, header: Optional[Mapping[str, str]] = None) -> None:
353 super(Fallback, self).__init__(address, or_port, dir_port, fingerprint, nickname, orport_v6)
354 self.has_extrainfo = has_extrainfo
355 self.header = collections.OrderedDict(header) if header else collections.OrderedDict()
357 @staticmethod
358 def from_cache(path: str = FALLBACK_CACHE_PATH) -> Dict[str, 'stem.directory.Fallback']:
359 conf = stem.util.conf.Config()
360 conf.load(path)
361 headers = collections.OrderedDict([(k.split('.', 1)[1], conf.get(k)) for k in conf.keys() if k.startswith('header.')])
363 results = {}
365 for fingerprint in set([key.split('.')[0] for key in conf.keys()]):
366 if fingerprint in ('tor_commit', 'stem_commit', 'header'):
367 continue
369 attr = {}
371 for attr_name in ('address', 'or_port', 'dir_port', 'nickname', 'has_extrainfo', 'orport6_address', 'orport6_port'):
372 key = '%s.%s' % (fingerprint, attr_name)
373 attr[attr_name] = conf.get(key)
375 if not attr[attr_name] and attr_name not in ('nickname', 'has_extrainfo', 'orport6_address', 'orport6_port'):
376 raise OSError("'%s' is missing from %s" % (key, FALLBACK_CACHE_PATH))
378 if attr['orport6_address'] and attr['orport6_port']:
379 orport_v6 = (attr['orport6_address'], int(attr['orport6_port']))
380 else:
381 orport_v6 = None
383 results[fingerprint] = Fallback(
384 address = attr['address'],
385 or_port = int(attr['or_port']),
386 dir_port = int(attr['dir_port']),
387 fingerprint = fingerprint,
388 nickname = attr['nickname'],
389 has_extrainfo = attr['has_extrainfo'] == 'true',
390 orport_v6 = orport_v6,
391 header = headers,
394 return results
396 @staticmethod
397 def from_remote(timeout: int = 60) -> Dict[str, 'stem.directory.Fallback']:
398 try:
399 lines = str_tools._to_unicode(urllib.request.urlopen(GITWEB_FALLBACK_URL, timeout = timeout).read()).splitlines()
401 if not lines:
402 raise OSError('no content')
403 except:
404 exc, stacktrace = sys.exc_info()[1:3]
405 message = "Unable to download tor's fallback directories from %s: %s" % (GITWEB_FALLBACK_URL, exc)
406 raise stem.DownloadFailed(GITWEB_FALLBACK_URL, exc, stacktrace, message)
408 # header metadata
410 if lines[0] != '/* type=fallback */':
411 raise OSError('%s does not have a type field indicating it is fallback directory metadata' % GITWEB_FALLBACK_URL)
413 header = {}
415 for line in Fallback._pop_section(lines):
416 mapping = FALLBACK_MAPPING.match(line)
418 if mapping:
419 header[mapping.group(1)] = mapping.group(2)
420 else:
421 raise OSError('Malformed fallback directory header line: %s' % line)
423 Fallback._pop_section(lines) # skip human readable comments
425 # Entries look like...
427 # "5.9.110.236:9030 orport=9001 id=0756B7CD4DFC8182BE23143FAC0642F515182CEB"
428 # " ipv6=[2a01:4f8:162:51e2::2]:9001"
429 # /* nickname=rueckgrat */
430 # /* extrainfo=1 */
432 try:
433 results = {}
435 for matches in _directory_entries(lines, Fallback._pop_section, (FALLBACK_ADDR, FALLBACK_NICKNAME, FALLBACK_EXTRAINFO, FALLBACK_IPV6), required = (FALLBACK_ADDR,)):
436 address, dir_port, or_port, fingerprint = matches[FALLBACK_ADDR] # type: ignore
438 results[fingerprint] = Fallback(
439 address = address,
440 or_port = int(or_port),
441 dir_port = int(dir_port),
442 fingerprint = fingerprint,
443 nickname = matches.get(FALLBACK_NICKNAME), # type: ignore
444 has_extrainfo = matches.get(FALLBACK_EXTRAINFO) == '1',
445 orport_v6 = matches.get(FALLBACK_IPV6), # type: ignore
446 header = header,
448 except ValueError as exc:
449 raise OSError(str(exc))
451 return results
453 @staticmethod
454 def _pop_section(lines: List[str]) -> List[str]:
456 Provides lines up through the next divider. This excludes lines with just a
457 comma since they're an artifact of these being C strings.
460 section_lines = []
462 if lines:
463 line = lines.pop(0)
465 while lines and line != FALLBACK_DIV:
466 if line.strip() != ',':
467 section_lines.append(line)
469 line = lines.pop(0)
471 return section_lines
473 @staticmethod
474 def _write(fallbacks: Dict[str, 'stem.directory.Fallback'], tor_commit: str, stem_commit: str, headers: Mapping[str, str], path: str = FALLBACK_CACHE_PATH) -> None:
476 Persists fallback directories to a location in a way that can be read by
477 from_cache().
479 :param fallbacks: mapping of fingerprints to their fallback directory
480 :param tor_commit: tor commit the fallbacks came from
481 :param stem_commit: stem commit the fallbacks came from
482 :param headers: metadata about the file these came from
483 :param path: location fallbacks will be persisted to
486 conf = stem.util.conf.Config()
487 conf.set('tor_commit', tor_commit)
488 conf.set('stem_commit', stem_commit)
490 for k, v in headers.items():
491 conf.set('header.%s' % k, v)
493 for directory in sorted(fallbacks.values(), key = lambda x: x.fingerprint):
494 fingerprint = directory.fingerprint
495 conf.set('%s.address' % fingerprint, directory.address)
496 conf.set('%s.or_port' % fingerprint, str(directory.or_port))
497 conf.set('%s.dir_port' % fingerprint, str(directory.dir_port))
498 conf.set('%s.nickname' % fingerprint, directory.nickname)
499 conf.set('%s.has_extrainfo' % fingerprint, 'true' if directory.has_extrainfo else 'false')
501 if directory.orport_v6:
502 conf.set('%s.orport6_address' % fingerprint, str(directory.orport_v6[0]))
503 conf.set('%s.orport6_port' % fingerprint, str(directory.orport_v6[1]))
505 conf.save(path)
507 def __hash__(self) -> int:
508 return stem.util._hash_attr(self, 'has_extrainfo', 'header', parent = Directory, cache = True)
510 def __eq__(self, other: Any) -> bool:
511 return hash(self) == hash(other) if isinstance(other, Fallback) else False
513 def __ne__(self, other: Any) -> bool:
514 return not self == other
517 def _fallback_directory_differences(previous_directories: Mapping[str, 'stem.directory.Fallback'], new_directories: Mapping[str, 'stem.directory.Fallback']) -> str:
519 Provides a description of how fallback directories differ.
522 lines = []
524 added_fp = set(new_directories.keys()).difference(previous_directories.keys())
525 removed_fp = set(previous_directories.keys()).difference(new_directories.keys())
527 for fp in added_fp:
528 directory = new_directories[fp]
529 orport_v6 = '%s:%s' % directory.orport_v6 if directory.orport_v6 else '[none]'
531 lines += [
532 '* Added %s as a new fallback directory:' % directory.fingerprint,
533 ' address: %s' % directory.address,
534 ' or_port: %s' % directory.or_port,
535 ' dir_port: %s' % directory.dir_port,
536 ' nickname: %s' % directory.nickname,
537 ' has_extrainfo: %s' % directory.has_extrainfo,
538 ' orport_v6: %s' % orport_v6,
542 for fp in removed_fp:
543 lines.append('* Removed %s as a fallback directory' % fp)
545 for fp in new_directories:
546 if fp in added_fp or fp in removed_fp:
547 continue # already discussed these
549 previous_directory = previous_directories[fp]
550 new_directory = new_directories[fp]
552 if previous_directory != new_directory:
553 for attr in ('address', 'or_port', 'dir_port', 'fingerprint', 'orport_v6', 'header'):
554 old_attr = getattr(previous_directory, attr)
555 new_attr = getattr(new_directory, attr)
557 if old_attr != new_attr:
558 lines.append('* Changed the %s of %s from %s to %s' % (attr, fp, old_attr, new_attr))
560 return '\n'.join(lines)
563 DIRECTORY_AUTHORITIES = {
564 'moria1': Authority(
565 nickname = 'moria1',
566 address = '128.31.0.39',
567 or_port = 9101,
568 dir_port = 9131,
569 fingerprint = '9695DFC35FFEB861329B9F1AB04C46397020CE31',
570 v3ident = 'D586D18309DED4CD6D57C18FDB97EFA96D330566',
572 'tor26': Authority(
573 nickname = 'tor26',
574 address = '86.59.21.38',
575 or_port = 443,
576 dir_port = 80,
577 fingerprint = '847B1F850344D7876491A54892F904934E4EB85D',
578 orport_v6 = ('2001:858:2:2:aabb:0:563b:1526', 443),
579 v3ident = '14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4',
581 'dizum': Authority(
582 nickname = 'dizum',
583 address = '45.66.33.45',
584 or_port = 443,
585 dir_port = 80,
586 fingerprint = '7EA6EAD6FD83083C538F44038BBFA077587DD755',
587 v3ident = 'E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58',
589 'gabelmoo': Authority(
590 nickname = 'gabelmoo',
591 address = '131.188.40.189',
592 or_port = 443,
593 dir_port = 80,
594 fingerprint = 'F2044413DAC2E02E3D6BCF4735A19BCA1DE97281',
595 orport_v6 = ('2001:638:a000:4140::ffff:189', 443),
596 v3ident = 'ED03BB616EB2F60BEC80151114BB25CEF515B226',
598 'dannenberg': Authority(
599 nickname = 'dannenberg',
600 address = '193.23.244.244',
601 or_port = 443,
602 dir_port = 80,
603 orport_v6 = ('2001:678:558:1000::244', 443),
604 fingerprint = '7BE683E65D48141321C5ED92F075C55364AC7123',
605 v3ident = '0232AF901C31A04EE9848595AF9BB7620D4C5B2E',
607 'maatuska': Authority(
608 nickname = 'maatuska',
609 address = '171.25.193.9',
610 or_port = 80,
611 dir_port = 443,
612 fingerprint = 'BD6A829255CB08E66FBE7D3748363586E46B3810',
613 orport_v6 = ('2001:67c:289c::9', 80),
614 v3ident = '49015F787433103580E3B66A1707A00E60F2D15B',
616 'Faravahar': Authority(
617 nickname = 'Faravahar',
618 address = '154.35.175.225',
619 or_port = 443,
620 dir_port = 80,
621 fingerprint = 'CF6D0AAFB385BE71B8E111FC5CFF4B47923733BC',
622 v3ident = 'EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97',
624 'longclaw': Authority(
625 nickname = 'longclaw',
626 address = '199.58.81.140',
627 or_port = 443,
628 dir_port = 80,
629 fingerprint = '74A910646BCEEFBCD2E874FC1DC997430F968145',
630 v3ident = '23D15D965BC35114467363C165C4F724B64B4F66',
632 'bastet': Authority(
633 nickname = 'bastet',
634 address = '204.13.164.118',
635 or_port = 443,
636 dir_port = 80,
637 fingerprint = '24E2F139121D4394C54B5BCC368B3B411857C413',
638 orport_v6 = ('2620:13:4000:6000::1000:118', 443),
639 v3ident = '27102BC123E7AF1D4741AE047E160C91ADC76B21',
641 'Serge': Authority(
642 nickname = 'Serge',
643 address = '66.111.2.131',
644 or_port = 9001,
645 dir_port = 9030,
646 fingerprint = 'BA44A889E64B93FAA2B114E02C2A279A8555C533',
647 v3ident = None, # does not vote in the consensus