relaylist.py 7.67 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

    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')

juga's avatar
juga committed
77
78
79
80
    @property
    def burst_bandwidth(self):
        return self._from_desc('burst_bandwidth')

81
82
83
84
    @property
    def observed_bandwidth(self):
        return self._from_desc('observed_bandwidth')

85
    @property
juga's avatar
juga committed
86
    def consensus_bandwidth(self):
87
88
        return self._from_ns('bandwidth')

juga's avatar
juga committed
89
90
91
92
93
94
95
    @property
    def consensus_bandwidth_is_unmeasured(self):
        # measured appears only votes, unmeasured appears in consensus
        # therefore is_unmeasured is needed to know whether the bandwidth
        # value in consensus is comming from bwauth measurements or not.
        return self._from_ns('is_unmeasured')

96
97
98
    @property
    def address(self):
        return self._from_ns('address')
juga's avatar
juga committed
99

100
    @property
juga's avatar
juga committed
101
    def master_key_ed25519(self):
102
103
104
        """Obtain ed25519 master key of the relay in server descriptors.

        :returns: str, the ed25519 master key base 64 encoded without
juga's avatar
juga committed
105
106
                  trailing '='s.

107
        """
juga's avatar
juga committed
108
109
        # Even if this key is called master-key-ed25519 in dir-spec.txt,
        # it seems that stem parses it as ed25519_master_key
110
111
112
113
        key = self._from_desc('ed25519_master_key')
        if key is None:
            return None
        return key.rstrip('=')
juga's avatar
juga committed
114

115
    def can_exit_to(self, host, port):
116
117
118
119
120
121
122
        '''
        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).
        '''
123
124
125
126
127
128
129
130
131
132
133
        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.
134
135
136
137
            results = resolve(host)
            if not len(results):
                return False
            host = results[0]
138
139
140
        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
141

Matt Traudt's avatar
Matt Traudt committed
142
class RelayList:
Matt Traudt's avatar
Matt Traudt committed
143
144
145
146
    ''' 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
147
148
    REFRESH_INTERVAL = 300  # seconds

Matt Traudt's avatar
Matt Traudt committed
149
150
    def __init__(self, args, conf, controller):
        self._controller = controller
151
        self.rng = random.SystemRandom()
152
        self._refresh_lock = Lock()
Matt Traudt's avatar
Matt Traudt committed
153
154
        self._refresh()

155
156
157
    def _need_refresh(self):
        return time.time() >= self._last_refresh + self.REFRESH_INTERVAL

Matt Traudt's avatar
Matt Traudt committed
158
159
    @property
    def relays(self):
160
161
162
        # 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():
163
164
            log.debug('We need to refresh our list of relays. '
                      'Going to wait for lock.')
165
166
167
            # 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:
168
169
                log.debug('We got the lock. Now to see if we still '
                          'need to refresh.')
170
171
172
173
                # 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():
174
                    log.debug('Yup we need to refresh our relays. Doing so.')
175
                    self._refresh()
176
177
178
179
                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
180
181
        return self._relays

Matt Traudt's avatar
Matt Traudt committed
182
183
184
185
    @property
    def fast(self):
        return self._relays_with_flag(Flag.FAST)

Matt Traudt's avatar
Matt Traudt committed
186
187
188
189
    @property
    def exits(self):
        return self._relays_with_flag(Flag.EXIT)

190
191
    @property
    def bad_exits(self):
192
        return self._relays_with_flag(Flag.BADEXIT)
193

194
195
196
197
    @property
    def non_exits(self):
        return self._relays_without_flag(Flag.EXIT)

Matt Traudt's avatar
Matt Traudt committed
198
199
200
201
    @property
    def guards(self):
        return self._relays_with_flag(Flag.GUARD)

202
203
204
205
    @property
    def authorities(self):
        return self._relays_with_flag(Flag.AUTHORITY)

Matt Traudt's avatar
Matt Traudt committed
206
    def random_relay(self):
207
        return self.rng.choice(self.relays)
Matt Traudt's avatar
Matt Traudt committed
208

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

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

Matt Traudt's avatar
Matt Traudt committed
215
216
    def _init_relays(self):
        c = self._controller
217
218
219
        try:
            relays = [Relay(ns.fingerprint, c, ns=ns)
                      for ns in c.get_network_statuses()]
220
        except ControllerError as e:
221
222
            log.exception("Exception trying to init relays %s", e)
            return []
juga's avatar
juga committed
223
        return relays
Matt Traudt's avatar
Matt Traudt committed
224
225
226

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