relaylist.py 7.25 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')

77
78
79
80
    @property
    def observed_bandwidth(self):
        return self._from_desc('observed_bandwidth')

81
82
83
84
85
86
87
    @property
    def bandwidth(self):
        return self._from_ns('bandwidth')

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

89
    @property
juga's avatar
juga committed
90
    def master_key_ed25519(self):
91
92
93
        """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
94
95
                  trailing '='s.

96
        """
juga's avatar
juga committed
97
98
        # Even if this key is called master-key-ed25519 in dir-spec.txt,
        # it seems that stem parses it as ed25519_master_key
99
100
101
102
        key = self._from_desc('ed25519_master_key')
        if key is None:
            return None
        return key.rstrip('=')
juga's avatar
juga committed
103

104
    def can_exit_to(self, host, port):
105
106
107
108
109
110
111
        '''
        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).
        '''
112
113
114
115
116
117
118
119
120
121
122
        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.
123
124
125
126
            results = resolve(host)
            if not len(results):
                return False
            host = results[0]
127
128
129
        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
130

Matt Traudt's avatar
Matt Traudt committed
131
class RelayList:
Matt Traudt's avatar
Matt Traudt committed
132
133
134
135
    ''' 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
136
137
    REFRESH_INTERVAL = 300  # seconds

Matt Traudt's avatar
Matt Traudt committed
138
139
    def __init__(self, args, conf, controller):
        self._controller = controller
140
        self.rng = random.SystemRandom()
141
        self._refresh_lock = Lock()
Matt Traudt's avatar
Matt Traudt committed
142
143
        self._refresh()

144
145
146
    def _need_refresh(self):
        return time.time() >= self._last_refresh + self.REFRESH_INTERVAL

Matt Traudt's avatar
Matt Traudt committed
147
148
    @property
    def relays(self):
149
150
151
        # 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():
152
153
            log.debug('We need to refresh our list of relays. '
                      'Going to wait for lock.')
154
155
156
            # 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:
157
158
                log.debug('We got the lock. Now to see if we still '
                          'need to refresh.')
159
160
161
162
                # 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():
163
                    log.debug('Yup we need to refresh our relays. Doing so.')
164
                    self._refresh()
165
166
167
168
                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
169
170
        return self._relays

Matt Traudt's avatar
Matt Traudt committed
171
172
173
174
    @property
    def fast(self):
        return self._relays_with_flag(Flag.FAST)

Matt Traudt's avatar
Matt Traudt committed
175
176
177
178
    @property
    def exits(self):
        return self._relays_with_flag(Flag.EXIT)

179
180
    @property
    def bad_exits(self):
181
        return self._relays_with_flag(Flag.BADEXIT)
182

183
184
185
186
    @property
    def non_exits(self):
        return self._relays_without_flag(Flag.EXIT)

Matt Traudt's avatar
Matt Traudt committed
187
188
189
190
    @property
    def guards(self):
        return self._relays_with_flag(Flag.GUARD)

191
192
193
194
    @property
    def authorities(self):
        return self._relays_with_flag(Flag.AUTHORITY)

Matt Traudt's avatar
Matt Traudt committed
195
    def random_relay(self):
196
        return self.rng.choice(self.relays)
Matt Traudt's avatar
Matt Traudt committed
197

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

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

Matt Traudt's avatar
Matt Traudt committed
204
205
    def _init_relays(self):
        c = self._controller
206
207
208
        try:
            relays = [Relay(ns.fingerprint, c, ns=ns)
                      for ns in c.get_network_statuses()]
209
        except ControllerError as e:
210
211
            log.exception("Exception trying to init relays %s", e)
            return []
juga's avatar
juga committed
212
        return relays
Matt Traudt's avatar
Matt Traudt committed
213
214
215

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