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 a7139bca Lars Kruse
#!/usr/bin/env python3
2 87325764 daftaupe
'''
3
=head1 NAME
4 f9d8ce70 Lars Kruse
5 17f78427 Lars Kruse
tor_
6 87325764 daftaupe
7
=head1 DESCRIPTION
8 f9d8ce70 Lars Kruse
9 8713eb37 Lars Kruse
Wildcard plugin that gathers some metrics from the Tor daemon
10 f9d8ce70 Lars Kruse
(https://github.com/daftaupe/munin-tor).
11 87325764 daftaupe
12
Derived from https://github.com/mweinelt/munin-tor
13
14 f9d8ce70 Lars Kruse
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 87325764 daftaupe
34 f9d8ce70 Lars Kruse
=item tor_traffic     - graph the read/written traffic
35
36
=back
37 87325764 daftaupe
38
=head2 CONFIGURATION
39 f9d8ce70 Lars Kruse
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 87325764 daftaupe
56
=head1 COPYRIGHT
57 f9d8ce70 Lars Kruse
58 87325764 daftaupe
MIT License
59
60 f9d8ce70 Lars Kruse
SPDX-License-Identifier: MIT
61
62 87325764 daftaupe
=head1 AUTHOR
63 f9d8ce70 Lars Kruse
64 452003a3 Pierre-Alain TORET
Pierre-Alain TORET <pierre-alain.toret@protonmail.com>
65 24ab44ca Lars Kruse
66
=head1 MAGIC MARKERS
67 f9d8ce70 Lars Kruse
68
 #%# family=auto
69
 #%# capabilities=autoconf suggest
70
71
=cut
72 87325764 daftaupe
'''
73 0d549a44 daftaupe
74
import collections
75
import json
76
import os
77 87325764 daftaupe
import sys
78
79 ec2d1fea daftaupe
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 0d549a44 daftaupe
89
default_torcachefile = 'munin_tor_country_stats.json'
90 87325764 daftaupe
default_torconnectmethod = 'port'
91 832ecbad wodry
default_torgeoippath = '/usr/share/GeoIP/GeoIP.dat'
92 87325764 daftaupe
default_tormaxcountries = 15
93 0d549a44 daftaupe
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 87325764 daftaupe
        print("Authentication failed (incorrect password)", file=sys.stderr)
122 0d549a44 daftaupe
123
124
def gen_controller():
125 87325764 daftaupe
    connect_method = os.environ.get('torconnectmethod', default_torconnectmethod)
126 0d549a44 daftaupe
    if connect_method == 'port':
127 24ab44ca Lars Kruse
        return stem.control.Controller.from_port(port=int(os.environ.get('torport',
128
                                                                         default_torport)))
129 0d549a44 daftaupe
    elif connect_method == 'socket':
130 24ab44ca Lars Kruse
        return stem.control.Controller.from_socket_file(path=os.environ.get('torsocket',
131
                                                                            default_torsocket))
132 0d549a44 daftaupe
    else:
133 24ab44ca Lars Kruse
        print("env.torconnectmethod contains an invalid value. "
134
              "Please specify either 'port' or 'socket'.", file=sys.stderr)
135 87325764 daftaupe
        sys.exit(1)
136 0d549a44 daftaupe
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 71f92805 Pierre-Alain TORET
        for key, val in graph.items():
154 0d549a44 daftaupe
            print('graph_{} {}'.format(key, val))
155
        # values
156 71f92805 Pierre-Alain TORET
        for label, attributes in labels.items():
157
            for key, val in attributes.items():
158 0d549a44 daftaupe
                print('{}.{} {}'.format(label, key, val))
159
160
    @staticmethod
161
    def autoconf():
162
        try:
163 87325764 daftaupe
            import stem
164
165
        except ImportError as e:
166 ec2d1fea daftaupe
            print('no (failed to import the required python module "stem": {})'.format(e))
167 87325764 daftaupe
168
        try:
169 24ab44ca Lars Kruse
            import GeoIP  # noqa: F401
170 87325764 daftaupe
171
        except ImportError as e:
172 ef913139 daftaupe
            print('no (failed to import the required python module "GeoIP": {})'.format(e))
173 87325764 daftaupe
174
        try:
175 0d549a44 daftaupe
            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 24ab44ca Lars Kruse
        options = ['bandwidth', 'connections', 'countries', 'dormant', 'flags', 'routers',
188
                   'traffic']
189 0d549a44 daftaupe
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 f14887f5 daftaupe
        graph = {'title': 'Tor observed bandwidth',
208 0d549a44 daftaupe
                 'args': '-l 0 --base 1000',
209
                 'vlabel': 'bytes/s',
210 87325764 daftaupe
                 'category': 'network',
211 0d549a44 daftaupe
                 '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 87325764 daftaupe
                sys.exit(1)
232 0d549a44 daftaupe
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 87325764 daftaupe
                sys.exit(1)
237 0d549a44 daftaupe
            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 f14887f5 daftaupe
        graph = {'title': 'Tor connections',
246 0d549a44 daftaupe
                 'args': '-l 0 --base 1000',
247
                 'vlabel': 'connections',
248 87325764 daftaupe
                 'category': 'network',
249 0d549a44 daftaupe
                 '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 87325764 daftaupe
                    sys.exit(1)
267 0d549a44 daftaupe
                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 71f92805 Pierre-Alain TORET
                    for state, count in states.items():
273 0d549a44 daftaupe
                        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 24ab44ca Lars Kruse
            self.cache_dir_name = os.path.join(
284
                self.cache_dir_name, os.environ.get('torcachefile', default_torcachefile))
285 0d549a44 daftaupe
286 87325764 daftaupe
        max_countries = os.environ.get('tormaxcountries', default_tormaxcountries)
287 0d549a44 daftaupe
        self.max_countries = int(max_countries)
288
289
        geoip_path = os.environ.get('torgeoippath', default_torgeoippath)
290 87325764 daftaupe
        self.geodb = GeoIP.open(geoip_path, GeoIP.GEOIP_MEMORY_CACHE)
291 0d549a44 daftaupe
292
    def conf(self):
293
        """Configure plugin"""
294
295 f14887f5 daftaupe
        graph = {'title': 'Tor countries',
296 0d549a44 daftaupe
                 'args': '-l 0 --base 1000',
297
                 'vlabel': 'countries',
298 87325764 daftaupe
                 'category': 'network',
299 0d549a44 daftaupe
                 '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 24ab44ca Lars Kruse
        except (IOError, ValueError):
322 0d549a44 daftaupe
            # 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 87325764 daftaupe
    @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 0d549a44 daftaupe
    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 87325764 daftaupe
            yield self.simplify(country)
353 0d549a44 daftaupe
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 f14887f5 daftaupe
        graph = {'title': 'Tor dormant',
372 0d549a44 daftaupe
                 'args': '-l 0 --base 1000',
373
                 'vlabel': 'dormant',
374 87325764 daftaupe
                 'category': 'network',
375 0d549a44 daftaupe
                 '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 87325764 daftaupe
                    sys.exit(1)
389 0d549a44 daftaupe
                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 f14887f5 daftaupe
        graph = {'title': 'Tor relay flags',
400 0d549a44 daftaupe
                 'args': '-l 0 --base 1000',
401
                 'vlabel': 'flags',
402 87325764 daftaupe
                 'category': 'network',
403 0d549a44 daftaupe
                 '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 87325764 daftaupe
                sys.exit(1)
424 0d549a44 daftaupe
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 87325764 daftaupe
                sys.exit(1)
429 0d549a44 daftaupe
            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 f14887f5 daftaupe
        graph = {'title': 'Tor routers',
442 0d549a44 daftaupe
                 'args': '-l 0',
443
                 'vlabel': 'routers',
444 87325764 daftaupe
                 'category': 'network',
445 0d549a44 daftaupe
                 'info': 'known Tor onion routers'}
446 24ab44ca Lars Kruse
        labels = {'routers': {'label': 'routers', 'min': 0, 'type': 'GAUGE'}}
447 0d549a44 daftaupe
        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 87325764 daftaupe
                sys.exit(1)
460 0d549a44 daftaupe
            else:
461
                routers = response.split('\n')
462 832ecbad wodry
                onr = 0
463
                for router in routers:
464
                    if router[0] == "r":
465
                        onr += 1
466 17f78427 Lars Kruse
467 0d549a44 daftaupe
                print('routers.value {}'.format(onr))
468
469
470
class TorTraffic(TorPlugin):
471
    def __init__(self):
472
        pass
473
474
    def conf(self):
475 f14887f5 daftaupe
        graph = {'title': 'Tor traffic',
476 0d549a44 daftaupe
                 'args': '-l 0 --base 1024',
477 168f6f92 Pierre-Alain TORET
                 'vlabel': 'bytes/s',
478 87325764 daftaupe
                 'category': 'network',
479 0d549a44 daftaupe
                 '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 87325764 daftaupe
                sys.exit(1)
497 0d549a44 daftaupe
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 87325764 daftaupe
                sys.exit(1)
504 0d549a44 daftaupe
            print('written.value {}'.format(response))
505
506
507
##########################
508 17f78427 Lars Kruse
# Main
509 0d549a44 daftaupe
##########################
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 24ab44ca Lars Kruse
            print('Unknown plugin name, try "suggest" for a list of possible ones.',
542
                  file=sys.stderr)
543 87325764 daftaupe
            sys.exit(1)
544 0d549a44 daftaupe
545
        if param == 'config':
546
            provider.conf()
547
        elif param == 'fetch':
548
            provider.fetch()
549
        else:
550 87325764 daftaupe
            print('Unknown parameter "{}"'.format(param), file=sys.stderr)
551 ec2d1fea daftaupe
            sys.exit(1)
552 0d549a44 daftaupe
553 24ab44ca Lars Kruse
554 0d549a44 daftaupe
if __name__ == '__main__':
555
    main()