relaylist.py 7.05 KB
Newer Older
juga's avatar
juga committed
1
from stem.descriptor.router_status_entry import RouterStatusEntryV3
2
from stem.descriptor.server_descriptor import ServerDescriptor
3
from stem import Flag, DescriptorUnavailable, ControllerError
4
5
from stem.util.connection import is_valid_ipv4_address
from stem.util.connection import is_valid_ipv6_address
Matt Traudt's avatar
Matt Traudt committed
6
import random
7
import time
8
import logging
9
from sbws.globals import resolve
10
from threading import Lock
Matt Traudt's avatar
Matt Traudt committed
11

12
13
log = logging.getLogger(__name__)

Matt Traudt's avatar
Matt Traudt committed
14

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Relay:
    def __init__(self, fp, cont, ns=None, desc=None):
        '''
        Given a relay fingerprint, fetch all the information about a relay that
        sbws currently needs and store it in this class. Acts as an abstraction
        to hide the confusion that is Tor consensus/descriptor stuff.

        :param str fp: fingerprint of the relay.
        :param cont: active and valid stem Tor controller connection
        '''
        assert isinstance(fp, str)
        assert len(fp) == 40
        if ns is not None:
            assert isinstance(ns, RouterStatusEntryV3)
            self._ns = ns
        else:
31
32
            try:
                self._ns = cont.get_network_status(fp, default=None)
33
            except (DescriptorUnavailable, ControllerError) as e:
34
                log.exception("Exception trying to get ns %s", e)
35
                self._ns = None
36
37
38
39
        if desc is not None:
            assert isinstance(desc, ServerDescriptor)
            self._desc = desc
        else:
40
41
            try:
                self._desc = cont.get_server_descriptor(fp, default=None)
42
            except (DescriptorUnavailable, ControllerError) as e:
juga's avatar
juga committed
43
                log.exception("Exception trying to get desc %s", e)
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

    def _from_desc(self, attr):
        if not self._desc:
            return None
        assert hasattr(self._desc, attr)
        return getattr(self._desc, attr)

    def _from_ns(self, attr):
        if not self._ns:
            return None
        assert hasattr(self._ns, attr)
        return getattr(self._ns, attr)

    @property
    def nickname(self):
        return self._from_ns('nickname')

    @property
    def fingerprint(self):
        return self._from_ns('fingerprint')

    @property
    def flags(self):
        return self._from_ns('flags')

    @property
    def exit_policy(self):
        return self._from_desc('exit_policy')

    @property
    def average_bandwidth(self):
        return self._from_desc('average_bandwidth')

    @property
    def bandwidth(self):
        return self._from_ns('bandwidth')

    @property
    def address(self):
        return self._from_ns('address')
juga's avatar
juga committed
84

85
    @property
juga's avatar
juga committed
86
    def master_key_ed25519(self):
87
88
89
90
91
        """Obtain ed25519 master key of the relay in server descriptors.

        :returns: str, the ed25519 master key base 64 encoded without
        trailing '='s.
        """
juga's avatar
juga committed
92
93
        # Even if this key is called master-key-ed25519 in dir-spec.txt,
        # it seems that stem parses it as ed25519_master_key
94
95
96
97
        key = self._from_desc('ed25519_master_key')
        if key is None:
            return None
        return key.rstrip('=')
juga's avatar
juga committed
98

99
    def can_exit_to(self, host, port):
100
101
102
103
104
105
106
        '''
        Returns if this relay can MOST LIKELY exit to the given host:port.
        **host** can be a hostname, but be warned that we will resolve it
        locally and use the first (arbitrary/unknown order) result when
        checking exit policies, which is different than what other parts of the
        code may do (leaving it up to the exit to resolve the name).
        '''
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
        if not self.exit_policy:
            return False
        assert isinstance(host, str)
        assert isinstance(port, int)
        if not is_valid_ipv4_address(host) and not is_valid_ipv6_address(host):
            # It certainly isn't perfect trying to guess if an exit can connect
            # to an ipv4/6 address based on the DNS result we got locally. But
            # it's the best we can do.
            #
            # Also, only use the first ipv4/6 we get even if there is more than
            # one.
            host = resolve(host)[0]
        assert is_valid_ipv4_address(host) or is_valid_ipv6_address(host)
        return self.exit_policy.can_exit_to(host, port)

juga's avatar
juga committed
122

Matt Traudt's avatar
Matt Traudt committed
123
class RelayList:
Matt Traudt's avatar
Matt Traudt committed
124
125
126
127
    ''' Keeps a list of all relays in the current Tor network and updates it
    transparently in the background. Provides useful interfaces for getting
    only relays of a certain type.
    '''
Matt Traudt's avatar
Matt Traudt committed
128
129
    REFRESH_INTERVAL = 300  # seconds

Matt Traudt's avatar
Matt Traudt committed
130
131
    def __init__(self, args, conf, controller):
        self._controller = controller
132
        self.rng = random.SystemRandom()
133
        self._refresh_lock = Lock()
Matt Traudt's avatar
Matt Traudt committed
134
135
        self._refresh()

136
137
138
    def _need_refresh(self):
        return time.time() >= self._last_refresh + self.REFRESH_INTERVAL

Matt Traudt's avatar
Matt Traudt committed
139
140
    @property
    def relays(self):
141
142
143
        # See if we can get the list of relays without having to do a refresh,
        # which is expensive and blocks other threads
        if self._need_refresh():
144
145
            log.debug('We need to refresh our list of relays. '
                      'Going to wait for lock.')
146
147
148
            # Whelp we couldn't just get the list of relays because the list is
            # stale. Wait for the lock so we can refresh it.
            with self._refresh_lock:
149
150
                log.debug('We got the lock. Now to see if we still '
                          'need to refresh.')
151
152
153
154
                # Now we have the lock ... but wait! Maybe someone else already
                # did the refreshing. So check if it still needs refreshing. If
                # not, we can do nothing.
                if self._need_refresh():
155
                    log.debug('Yup we need to refresh our relays. Doing so.')
156
                    self._refresh()
157
158
159
160
                else:
                    log.debug('No we don\'t need to refresh our relays. '
                              'It was done by someone else.')
            log.debug('Giving back the lock for refreshing relays.')
Matt Traudt's avatar
Matt Traudt committed
161
162
        return self._relays

Matt Traudt's avatar
Matt Traudt committed
163
164
165
166
    @property
    def fast(self):
        return self._relays_with_flag(Flag.FAST)

Matt Traudt's avatar
Matt Traudt committed
167
168
169
170
    @property
    def exits(self):
        return self._relays_with_flag(Flag.EXIT)

171
172
    @property
    def bad_exits(self):
173
        return self._relays_with_flag(Flag.BADEXIT)
174

175
176
177
178
    @property
    def non_exits(self):
        return self._relays_without_flag(Flag.EXIT)

Matt Traudt's avatar
Matt Traudt committed
179
180
181
182
    @property
    def guards(self):
        return self._relays_with_flag(Flag.GUARD)

183
184
185
186
    @property
    def authorities(self):
        return self._relays_with_flag(Flag.AUTHORITY)

Matt Traudt's avatar
Matt Traudt committed
187
    def random_relay(self):
188
        return self.rng.choice(self.relays)
Matt Traudt's avatar
Matt Traudt committed
189

Matt Traudt's avatar
Matt Traudt committed
190
    def _relays_with_flag(self, flag):
191
        return [r for r in self.relays if flag in r.flags]
Matt Traudt's avatar
Matt Traudt committed
192

Matt Traudt's avatar
Matt Traudt committed
193
    def _relays_without_flag(self, flag):
194
        return [r for r in self.relays if flag not in r.flags]
Matt Traudt's avatar
Matt Traudt committed
195

Matt Traudt's avatar
Matt Traudt committed
196
197
    def _init_relays(self):
        c = self._controller
198
199
200
        try:
            relays = [Relay(ns.fingerprint, c, ns=ns)
                      for ns in c.get_network_statuses()]
201
        except ControllerError as e:
202
203
            log.exception("Exception trying to init relays %s", e)
            return []
juga's avatar
juga committed
204
        return relays
Matt Traudt's avatar
Matt Traudt committed
205
206
207

    def _refresh(self):
        self._relays = self._init_relays()
208
        self._last_refresh = time.time()