Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / tor / tor_ @ a7139bca

Historique | Voir | Annoter | Télécharger (17,5 ko)

1
#!/usr/bin/env python3
2
'''
3
=head1 NAME
4

    
5
tor_
6

    
7
=head1 DESCRIPTION
8

    
9
Wildcard plugin that gathers some metrics from the Tor daemon
10
(https://github.com/daftaupe/munin-tor).
11

    
12
Derived from https://github.com/mweinelt/munin-tor
13

    
14
This plugin requires the stem library (https://stem.torproject.org/).
15

    
16
This plugin requires the GeoIP library (https://www.maxmind.com) for the countries plugin.
17

    
18
Available plugins:
19

    
20
=over 4
21

    
22
=item tor_bandwidth   - graph the glabal bandwidth
23

    
24
=item tor_connections - graph the number of connexions
25

    
26
=item tor_countries   - graph the countries represented our connexions
27

    
28
=item tor_dormant     - graph if tor is dormant or not
29

    
30
=item tor_flags       - graph the different flags of the relay
31

    
32
=item tor_routers     - graph the number of routers seen by the relay
33

    
34
=item tor_traffic     - graph the read/written traffic
35

    
36
=back
37

    
38
=head2 CONFIGURATION
39

    
40
The default configuration is:
41

    
42
 [tor_*]
43
 user toranon # or any other user/group that is running tor
44
 group toranon
45
 env.torcachefile munin_tor_country_stats.json
46
 env.torconnectmethod port
47
 env.torgeoippath /usr/share/GeoIP/GeoIP.dat
48
 env.tormaxcountries 15
49
 env.torport 9051
50
 env.torsocket /var/run/tor/control
51

    
52
To make it connect through a socket, you simply need to change C<torconnectmethod>:
53

    
54
 env.torconnectmethod socket
55

    
56
=head1 COPYRIGHT
57

    
58
MIT License
59

    
60
SPDX-License-Identifier: MIT
61

    
62
=head1 AUTHOR
63

    
64
Pierre-Alain TORET <pierre-alain.toret@protonmail.com>
65

    
66
=head1 MAGIC MARKERS
67

    
68
 #%# family=auto
69
 #%# capabilities=autoconf suggest
70

    
71
=cut
72
'''
73

    
74
import collections
75
import json
76
import os
77
import sys
78

    
79
try:
80
    import GeoIP
81
    import stem
82
    import stem.control
83
    import stem.connection
84
except ImportError:
85
    # missing dependencies are reported via "autoconf"
86
    # thus failure is acceptable here
87
    pass
88

    
89
default_torcachefile = 'munin_tor_country_stats.json'
90
default_torconnectmethod = 'port'
91
default_torgeoippath = '/usr/share/GeoIP/GeoIP.dat'
92
default_tormaxcountries = 15
93
default_torport = 9051
94
default_torsocket = '/var/run/tor/control'
95

    
96

    
97
class ConnectionError(Exception):
98
    """Error connecting to the controller"""
99

    
100

    
101
class AuthError(Exception):
102
    """Error authenticating to the controller"""
103

    
104

    
105
def authenticate(controller):
106
    try:
107
        controller.authenticate()
108
        return
109
    except stem.connection.MissingPassword:
110
        pass
111

    
112
    try:
113
        password = os.environ['torpassword']
114
    except KeyError:
115
        raise AuthError("Please configure the 'torpassword' "
116
                        "environment variable")
117

    
118
    try:
119
        controller.authenticate(password=password)
120
    except stem.connection.PasswordAuthFailed:
121
        print("Authentication failed (incorrect password)", file=sys.stderr)
122

    
123

    
124
def gen_controller():
125
    connect_method = os.environ.get('torconnectmethod', default_torconnectmethod)
126
    if connect_method == 'port':
127
        return stem.control.Controller.from_port(port=int(os.environ.get('torport',
128
                                                                         default_torport)))
129
    elif connect_method == 'socket':
130
        return stem.control.Controller.from_socket_file(path=os.environ.get('torsocket',
131
                                                                            default_torsocket))
132
    else:
133
        print("env.torconnectmethod contains an invalid value. "
134
              "Please specify either 'port' or 'socket'.", file=sys.stderr)
135
        sys.exit(1)
136

    
137

    
138
#########################
139
# Base Class
140
#########################
141

    
142

    
143
class TorPlugin(object):
144
    def __init__(self):
145
        raise NotImplementedError
146

    
147
    def conf(self):
148
        raise NotImplementedError
149

    
150
    @staticmethod
151
    def conf_from_dict(graph, labels):
152
        # header
153
        for key, val in graph.items():
154
            print('graph_{} {}'.format(key, val))
155
        # values
156
        for label, attributes in labels.items():
157
            for key, val in attributes.items():
158
                print('{}.{} {}'.format(label, key, val))
159

    
160
    @staticmethod
161
    def autoconf():
162
        try:
163
            import stem
164

    
165
        except ImportError as e:
166
            print('no (failed to import the required python module "stem": {})'.format(e))
167

    
168
        try:
169
            import GeoIP  # noqa: F401
170

    
171
        except ImportError as e:
172
            print('no (failed to import the required python module "GeoIP": {})'.format(e))
173

    
174
        try:
175
            with gen_controller() as controller:
176
                try:
177
                    authenticate(controller)
178
                    print('yes')
179
                except stem.connection.AuthenticationFailure as e:
180
                    print('no (Authentication failed: {})'.format(e))
181

    
182
        except stem.connection:
183
            print('no (Connection failed)')
184

    
185
    @staticmethod
186
    def suggest():
187
        options = ['bandwidth', 'connections', 'countries', 'dormant', 'flags', 'routers',
188
                   'traffic']
189

    
190
        for option in options:
191
            print(option)
192

    
193
    def fetch(self):
194
        raise NotImplementedError
195

    
196

    
197
##########################
198
# Child Classes
199
##########################
200

    
201

    
202
class TorBandwidth(TorPlugin):
203
    def __init__(self):
204
        pass
205

    
206
    def conf(self):
207
        graph = {'title': 'Tor observed bandwidth',
208
                 'args': '-l 0 --base 1000',
209
                 'vlabel': 'bytes/s',
210
                 'category': 'network',
211
                 'info': 'estimated capacity based on usage in bytes/s'}
212
        labels = {'bandwidth': {'label': 'bandwidth', 'min': 0, 'type': 'GAUGE'}}
213

    
214
        TorPlugin.conf_from_dict(graph, labels)
215

    
216
    def fetch(self):
217
        with gen_controller() as controller:
218
            try:
219
                authenticate(controller)
220
            except stem.connection.AuthenticationFailure as e:
221
                print('Authentication failed ({})'.format(e))
222
                return
223

    
224
            # Get fingerprint of our own relay to look up the descriptor for.
225
            # In Stem 1.3.0 and later, get_server_descriptor() will fetch the
226
            # relay's own descriptor if no argument is provided, so this will
227
            # no longer be needed.
228
            fingerprint = controller.get_info('fingerprint', None)
229
            if fingerprint is None:
230
                print("Error while reading fingerprint from Tor daemon", file=sys.stderr)
231
                sys.exit(1)
232

    
233
            response = controller.get_server_descriptor(fingerprint, None)
234
            if response is None:
235
                print("Error while getting server descriptor from Tor daemon", file=sys.stderr)
236
                sys.exit(1)
237
            print('bandwidth.value {}'.format(response.observed_bandwidth))
238

    
239

    
240
class TorConnections(TorPlugin):
241
    def __init__(self):
242
        pass
243

    
244
    def conf(self):
245
        graph = {'title': 'Tor connections',
246
                 'args': '-l 0 --base 1000',
247
                 'vlabel': 'connections',
248
                 'category': 'network',
249
                 'info': 'OR connections by state'}
250
        labels = {'new': {'label': 'new', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
251
                  'launched': {'label': 'launched', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
252
                  'connected': {'label': 'connected', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
253
                  'failed': {'label': 'failed', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
254
                  'closed': {'label': 'closed', 'min': 0, 'max': 25000, 'type': 'GAUGE'}}
255

    
256
        TorPlugin.conf_from_dict(graph, labels)
257

    
258
    def fetch(self):
259
        with gen_controller() as controller:
260
            try:
261
                authenticate(controller)
262

    
263
                response = controller.get_info('orconn-status', None)
264
                if response is None:
265
                    print("No response from Tor daemon in TorConnection.fetch()", file=sys.stderr)
266
                    sys.exit(1)
267
                else:
268
                    connections = response.split('\n')
269
                    states = dict((state, 0) for state in stem.ORStatus)
270
                    for connection in connections:
271
                        states[connection.rsplit(None, 1)[-1]] += 1
272
                    for state, count in states.items():
273
                        print('{}.value {}'.format(state.lower(), count))
274
            except stem.connection.AuthenticationFailure as e:
275
                print('Authentication failed ({})'.format(e))
276

    
277

    
278
class TorCountries(TorPlugin):
279
    def __init__(self):
280
        # Configure plugin
281
        self.cache_dir_name = os.environ.get('torcachedir', None)
282
        if self.cache_dir_name is not None:
283
            self.cache_dir_name = os.path.join(
284
                self.cache_dir_name, os.environ.get('torcachefile', default_torcachefile))
285

    
286
        max_countries = os.environ.get('tormaxcountries', default_tormaxcountries)
287
        self.max_countries = int(max_countries)
288

    
289
        geoip_path = os.environ.get('torgeoippath', default_torgeoippath)
290
        self.geodb = GeoIP.open(geoip_path, GeoIP.GEOIP_MEMORY_CACHE)
291

    
292
    def conf(self):
293
        """Configure plugin"""
294

    
295
        graph = {'title': 'Tor countries',
296
                 'args': '-l 0 --base 1000',
297
                 'vlabel': 'countries',
298
                 'category': 'network',
299
                 'info': 'OR connections by state'}
300
        labels = {}
301

    
302
        countries_num = self.top_countries()
303

    
304
        for c, v in countries_num:
305
            labels[c] = {'label': c, 'min': 0, 'max': 25000, 'type': 'GAUGE'}
306

    
307
        TorPlugin.conf_from_dict(graph, labels)
308

    
309
        # If needed, create cache file at config time
310
        if self.cache_dir_name:
311
            with open(self.cache_dir_name, 'w') as f:
312
                json.dump(countries_num, f)
313

    
314
    def fetch(self):
315
        """Generate metrics"""
316

    
317
        # If possible, read cached data instead of doing the processing twice
318
        try:
319
            with open(self.cache_dir_name) as f:
320
                countries_num = json.load(f)
321
        except (IOError, ValueError):
322
            # Fallback if cache_dir_name is not set, unreadable or any other
323
            # error
324
            countries_num = self.top_countries()
325

    
326
        for c, v in countries_num:
327
            print("%s.value %d" % (c, v))
328

    
329
    @staticmethod
330
    def _gen_ipaddrs_from_statuses(controller):
331
        """Generate a sequence of ipaddrs for every network status"""
332
        for desc in controller.get_network_statuses():
333
            ipaddr = desc.address
334
            yield ipaddr
335

    
336
    @staticmethod
337
    def simplify(cn):
338
        """Simplify country name"""
339
        cn = cn.replace(' ', '_')
340
        cn = cn.replace("'", '_')
341
        cn = cn.split(',', 1)[0]
342
        return cn
343

    
344
    def _gen_countries(self, controller):
345
        """Generate a sequence of countries for every built circuit"""
346
        for ipaddr in self._gen_ipaddrs_from_statuses(controller):
347
            country = self.geodb.country_name_by_addr(ipaddr)
348
            if country is None:
349
                yield 'Unknown'
350
                continue
351

    
352
            yield self.simplify(country)
353

    
354
    def top_countries(self):
355
        """Build a list of top countries by number of circuits"""
356
        with gen_controller() as controller:
357
            try:
358
                authenticate(controller)
359
                c = collections.Counter(self._gen_countries(controller))
360
                return sorted(c.most_common(self.max_countries))
361
            except stem.connection.AuthenticationFailure as e:
362
                print('Authentication failed ({})'.format(e))
363
                return []
364

    
365

    
366
class TorDormant(TorPlugin):
367
    def __init__(self):
368
        pass
369

    
370
    def conf(self):
371
        graph = {'title': 'Tor dormant',
372
                 'args': '-l 0 --base 1000',
373
                 'vlabel': 'dormant',
374
                 'category': 'network',
375
                 'info': 'Is Tor not building circuits because it is idle?'}
376
        labels = {'dormant': {'label': 'dormant', 'min': 0, 'max': 1, 'type': 'GAUGE'}}
377

    
378
        TorPlugin.conf_from_dict(graph, labels)
379

    
380
    def fetch(self):
381
        with gen_controller() as controller:
382
            try:
383
                authenticate(controller)
384

    
385
                response = controller.get_info('dormant', None)
386
                if response is None:
387
                    print("Error while reading dormant state from Tor daemon", file=sys.stderr)
388
                    sys.exit(1)
389
                print('dormant.value {}'.format(response))
390
            except stem.connection.AuthenticationFailure as e:
391
                print('Authentication failed ({})'.format(e))
392

    
393

    
394
class TorFlags(TorPlugin):
395
    def __init__(self):
396
        pass
397

    
398
    def conf(self):
399
        graph = {'title': 'Tor relay flags',
400
                 'args': '-l 0 --base 1000',
401
                 'vlabel': 'flags',
402
                 'category': 'network',
403
                 'info': 'Flags active for relay'}
404
        labels = {flag: {'label': flag, 'min': 0, 'max': 1, 'type': 'GAUGE'} for flag in stem.Flag}
405

    
406
        TorPlugin.conf_from_dict(graph, labels)
407

    
408
    def fetch(self):
409
        with gen_controller() as controller:
410
            try:
411
                authenticate(controller)
412
            except stem.connection.AuthenticationFailure as e:
413
                print('Authentication failed ({})'.format(e))
414
                return
415

    
416
            # Get fingerprint of our own relay to look up the status entry for.
417
            # In Stem 1.3.0 and later, get_network_status() will fetch the
418
            # relay's own status entry if no argument is provided, so this will
419
            # no longer be needed.
420
            fingerprint = controller.get_info('fingerprint', None)
421
            if fingerprint is None:
422
                print("Error while reading fingerprint from Tor daemon", file=sys.stderr)
423
                sys.exit(1)
424

    
425
            response = controller.get_network_status(fingerprint, None)
426
            if response is None:
427
                print("Error while getting server descriptor from Tor daemon", file=sys.stderr)
428
                sys.exit(1)
429
            for flag in stem.Flag:
430
                if flag in response.flags:
431
                    print('{}.value 1'.format(flag))
432
                else:
433
                    print('{}.value 0'.format(flag))
434

    
435

    
436
class TorRouters(TorPlugin):
437
    def __init__(self):
438
        pass
439

    
440
    def conf(self):
441
        graph = {'title': 'Tor routers',
442
                 'args': '-l 0',
443
                 'vlabel': 'routers',
444
                 'category': 'network',
445
                 'info': 'known Tor onion routers'}
446
        labels = {'routers': {'label': 'routers', 'min': 0, 'type': 'GAUGE'}}
447
        TorPlugin.conf_from_dict(graph, labels)
448

    
449
    def fetch(self):
450
        with gen_controller() as controller:
451
            try:
452
                authenticate(controller)
453
            except stem.connection.AuthenticationFailure as e:
454
                print('Authentication failed ({})'.format(e))
455
                return
456
            response = controller.get_info('ns/all', None)
457
            if response is None:
458
                print("Error while reading ns/all from Tor daemon", file=sys.stderr)
459
                sys.exit(1)
460
            else:
461
                routers = response.split('\n')
462
                onr = 0
463
                for router in routers:
464
                    if router[0] == "r":
465
                        onr += 1
466

    
467
                print('routers.value {}'.format(onr))
468

    
469

    
470
class TorTraffic(TorPlugin):
471
    def __init__(self):
472
        pass
473

    
474
    def conf(self):
475
        graph = {'title': 'Tor traffic',
476
                 'args': '-l 0 --base 1024',
477
                 'vlabel': 'bytes/s',
478
                 'category': 'network',
479
                 'info': 'bytes read/written'}
480
        labels = {'read': {'label': 'read', 'min': 0, 'type': 'DERIVE'},
481
                  'written': {'label': 'written', 'min': 0, 'type': 'DERIVE'}}
482

    
483
        TorPlugin.conf_from_dict(graph, labels)
484

    
485
    def fetch(self):
486
        with gen_controller() as controller:
487
            try:
488
                authenticate(controller)
489
            except stem.connection.AuthenticationFailure as e:
490
                print('Authentication failed ({})'.format(e))
491
                return
492

    
493
            response = controller.get_info('traffic/read', None)
494
            if response is None:
495
                print("Error while reading traffic/read from Tor daemon", file=sys.stderr)
496
                sys.exit(1)
497

    
498
            print('read.value {}'.format(response))
499

    
500
            response = controller.get_info('traffic/written', None)
501
            if response is None:
502
                print("Error while reading traffic/write from Tor daemon", file=sys.stderr)
503
                sys.exit(1)
504
            print('written.value {}'.format(response))
505

    
506

    
507
##########################
508
# Main
509
##########################
510

    
511

    
512
def main():
513
    if len(sys.argv) > 1:
514
        param = sys.argv[1].lower()
515
    else:
516
        param = 'fetch'
517

    
518
    if param == 'autoconf':
519
        TorPlugin.autoconf()
520
        sys.exit()
521
    elif param == 'suggest':
522
        TorPlugin.suggest()
523
        sys.exit()
524
    else:
525
        # detect data provider
526
        if __file__.endswith('_bandwidth'):
527
            provider = TorBandwidth()
528
        elif __file__.endswith('_connections'):
529
            provider = TorConnections()
530
        elif __file__.endswith('_countries'):
531
            provider = TorCountries()
532
        elif __file__.endswith('_dormant'):
533
            provider = TorDormant()
534
        elif __file__.endswith('_flags'):
535
            provider = TorFlags()
536
        elif __file__.endswith('_routers'):
537
            provider = TorRouters()
538
        elif __file__.endswith('_traffic'):
539
            provider = TorTraffic()
540
        else:
541
            print('Unknown plugin name, try "suggest" for a list of possible ones.',
542
                  file=sys.stderr)
543
            sys.exit(1)
544

    
545
        if param == 'config':
546
            provider.conf()
547
        elif param == 'fetch':
548
            provider.fetch()
549
        else:
550
            print('Unknown parameter "{}"'.format(param), file=sys.stderr)
551
            sys.exit(1)
552

    
553

    
554
if __name__ == '__main__':
555
    main()