relaylist.py 6.91 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
Matt Traudt's avatar
Matt Traudt committed
3
from stem import Flag
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
33
34
            try:
                self._ns = cont.get_network_status(fp, default=None)
            except Exception as e:
                log.exception("Exception trying to get ns %s", e)
35
36
37
38
        if desc is not None:
            assert isinstance(desc, ServerDescriptor)
            self._desc = desc
        else:
39
40
41
42
            try:
                self._desc = cont.get_server_descriptor(fp, default=None)
            except Exception as e:
                log.exception("Exception trying to get ns %s", e)
43
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

    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
83

84
    @property
juga's avatar
juga committed
85
    def master_key_ed25519(self):
86
87
88
89
90
        """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
91
92
        # Even if this key is called master-key-ed25519 in dir-spec.txt,
        # it seems that stem parses it as ed25519_master_key
93
94
95
96
        key = self._from_desc('ed25519_master_key')
        if key is None:
            return None
        return key.rstrip('=')
juga's avatar
juga committed
97

98
    def can_exit_to(self, host, port):
99
100
101
102
103
104
105
        '''
        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).
        '''
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
        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
121

Matt Traudt's avatar
Matt Traudt committed
122
class RelayList:
Matt Traudt's avatar
Matt Traudt committed
123
124
125
126
    ''' 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
127
128
    REFRESH_INTERVAL = 300  # seconds

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

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

Matt Traudt's avatar
Matt Traudt committed
138
139
    @property
    def relays(self):
140
141
142
        # 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():
143
144
            log.debug('We need to refresh our list of relays. '
                      'Going to wait for lock.')
145
146
147
            # 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:
148
149
                log.debug('We got the lock. Now to see if we still '
                          'need to refresh.')
150
151
152
153
                # 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():
154
                    log.debug('Yup we need to refresh our relays. Doing so.')
155
                    self._refresh()
156
157
158
159
                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
160
161
        return self._relays

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

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

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

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

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

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

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

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

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

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

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