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

130
131
132
133
134
135
136
137
138
139
140
    def can_exit_to_port(self, port):
        """
        Returns True if the relay has an exit policy and the policy accepts
        exiting to the given portself or False otherwise.
        """
        assert isinstance(port, int)
        # if dind't get the descriptor, there isn't exit policy
        if not self.exit_policy:
            return False
        return self.exit_policy.can_exit_to(port=port)

141
142
143
144
145
    def is_exit_not_bad_allowing_port(self, port):
        return (Flag.BADEXIT not in self.flags and
                Flag.EXIT in self.flags and
                self.can_exit_to_port(port))

juga's avatar
juga committed
146

Matt Traudt's avatar
Matt Traudt committed
147
class RelayList:
Matt Traudt's avatar
Matt Traudt committed
148
149
150
151
    ''' 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
152
153
    REFRESH_INTERVAL = 300  # seconds

Matt Traudt's avatar
Matt Traudt committed
154
155
    def __init__(self, args, conf, controller):
        self._controller = controller
156
        self.rng = random.SystemRandom()
157
        self._refresh_lock = Lock()
Matt Traudt's avatar
Matt Traudt committed
158
159
        self._refresh()

160
161
162
    def _need_refresh(self):
        return time.time() >= self._last_refresh + self.REFRESH_INTERVAL

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

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

Matt Traudt's avatar
Matt Traudt committed
191
192
193
194
    @property
    def exits(self):
        return self._relays_with_flag(Flag.EXIT)

195
196
    @property
    def bad_exits(self):
197
        return self._relays_with_flag(Flag.BADEXIT)
198

199
200
201
202
    @property
    def non_exits(self):
        return self._relays_without_flag(Flag.EXIT)

Matt Traudt's avatar
Matt Traudt committed
203
204
205
206
    @property
    def guards(self):
        return self._relays_with_flag(Flag.GUARD)

207
208
209
210
    @property
    def authorities(self):
        return self._relays_with_flag(Flag.AUTHORITY)

Matt Traudt's avatar
Matt Traudt committed
211
    def random_relay(self):
212
        return self.rng.choice(self.relays)
Matt Traudt's avatar
Matt Traudt committed
213

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

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

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

    def _refresh(self):
        self._relays = self._init_relays()
232
        self._last_refresh = time.time()
233
234
235
236
237


    def exits_not_bad_allowing_port(self, port):
        return [r for r in self.exits
                if r.is_exit_not_bad_allowing_port(port)]