diff --git a/plugins/tor/example-graphs/tor_-1.png b/plugins/tor/example-graphs/tor_-1.png new file mode 100644 index 00000000..d899769f Binary files /dev/null and b/plugins/tor/example-graphs/tor_-1.png differ diff --git a/plugins/tor/example-graphs/tor_-2.png b/plugins/tor/example-graphs/tor_-2.png new file mode 100644 index 00000000..822cc4c6 Binary files /dev/null and b/plugins/tor/example-graphs/tor_-2.png differ diff --git a/plugins/tor/example-graphs/tor_-3.png b/plugins/tor/example-graphs/tor_-3.png new file mode 100644 index 00000000..aaa0cf7d Binary files /dev/null and b/plugins/tor/example-graphs/tor_-3.png differ diff --git a/plugins/tor/example-graphs/tor_-4.png b/plugins/tor/example-graphs/tor_-4.png new file mode 100644 index 00000000..3a3ddc14 Binary files /dev/null and b/plugins/tor/example-graphs/tor_-4.png differ diff --git a/plugins/tor/example-graphs/tor_-5.png b/plugins/tor/example-graphs/tor_-5.png new file mode 100644 index 00000000..d40b7eb8 Binary files /dev/null and b/plugins/tor/example-graphs/tor_-5.png differ diff --git a/plugins/tor/example-graphs/tor_-6.png b/plugins/tor/example-graphs/tor_-6.png new file mode 100644 index 00000000..e1ae6c7e Binary files /dev/null and b/plugins/tor/example-graphs/tor_-6.png differ diff --git a/plugins/tor/example-graphs/tor_-7.png b/plugins/tor/example-graphs/tor_-7.png new file mode 100644 index 00000000..c7f67fa3 Binary files /dev/null and b/plugins/tor/example-graphs/tor_-7.png differ diff --git a/plugins/tor/tor_ b/plugins/tor/tor_ new file mode 100755 index 00000000..18929d3d --- /dev/null +++ b/plugins/tor/tor_ @@ -0,0 +1,539 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +''' +=head1 NAME +tor_ + +=head1 DESCRIPTION +Wildcard plugin that gathers some metrics from the Tor deamon +https://github.com/daftaupe/munin-tor + +Derived from https://github.com/mweinelt/munin-tor + +This plugin requires the stem library : https://stem.torproject.org/ +This plugin requires the GeoIP library : https://www.maxmind.com for the countries plugin + +Available plugins : +tor_bandwidth # Graph the glabal bandwidth +tor_connections # Graph the number of connexions +tor_countries # Graph the countries represented our connexions +tor_dormant # Graph if tor is dormant or not +tor_flags # Graph the different flags of the relay +tor_routers # Graph the number of routers seen by the relay +tor_traffic # Graph the read/written traffic + +=head2 CONFIGURATION +The default configuration is below +[tor_*] +user toranon # or any other user/group that is running tor +group toranon +env.torcachefile 'munin_tor_country_stats.json' +env.torconnectmethod 'port' +env.torgeoippath "/usr/share/GeoIP/GeoIP.dat" +env.tormaxcountries 15 +env.torport 9051 +env.torsocket '/var/run/tor/control' + +To make it connect through a socket modify this way +[tor_*] +user toranon # or any other user/group that is running tor +group toranon +env.torcachefile 'munin_tor_country_stats.json' +env.torconnectmethod 'socket' +env.torgeoippath "/usr/share/GeoIP/GeoIP.dat" +env.tormaxcountries 15 +env.torport 9051 +env.torsocket '/var/run/tor/control' + +=head1 COPYRIGHT +MIT License + +=head1 AUTHOR +daftaupe +''' + +from __future__ import print_function +import collections +import json +import os +import sys + +try: + import GeoIP + import stem + import stem.control + import stem.connection +except ImportError: + # missing dependencies are reported via "autoconf" + # thus failure is acceptable here + pass + +default_torcachefile = 'munin_tor_country_stats.json' +default_torconnectmethod = 'port' +default_torgeoippath = "/usr/share/GeoIP/GeoIP.dat" +default_tormaxcountries = 15 +default_torport = 9051 +default_torsocket = '/var/run/tor/control' + +#%# family=auto +#%# capabilities=autoconf suggest + + + +class ConnectionError(Exception): + """Error connecting to the controller""" + + +class AuthError(Exception): + """Error authenticating to the controller""" + + +def authenticate(controller): + try: + controller.authenticate() + return + except stem.connection.MissingPassword: + pass + + try: + password = os.environ['torpassword'] + except KeyError: + raise AuthError("Please configure the 'torpassword' " + "environment variable") + + try: + controller.authenticate(password=password) + except stem.connection.PasswordAuthFailed: + print("Authentication failed (incorrect password)", file=sys.stderr) + + +def gen_controller(): + connect_method = os.environ.get('torconnectmethod', default_torconnectmethod) + if connect_method == 'port': + return stem.control.Controller.from_port(port=int(os.environ.get('torport', default_torport))) + elif connect_method == 'socket': + return stem.control.Controller.from_socket_file(path=os.environ.get('torsocket', default_torsocket)) + else: + print("env.torconnectmethod contains an invalid value. Please specify either 'port' or 'socket'.", file=sys.stderr) + sys.exit(1) + + +######################### +# Base Class +######################### + + +class TorPlugin(object): + def __init__(self): + raise NotImplementedError + + def conf(self): + raise NotImplementedError + + @staticmethod + def conf_from_dict(graph, labels): + # header + for key, val in graph.iteritems(): + print('graph_{} {}'.format(key, val)) + # values + for label, attributes in labels.iteritems(): + for key, val in attributes.iteritems(): + print('{}.{} {}'.format(label, key, val)) + + @staticmethod + def autoconf(): + try: + import stem + + except ImportError as e: + print('no (failed to import the required python module "stem": {})'.format(e)) + + try: + import GeoIP + + except ImportError as e: + print('no (failed to import the required python module "GeoIP": {})'.format(e)) + + try: + with gen_controller() as controller: + try: + authenticate(controller) + print('yes') + except stem.connection.AuthenticationFailure as e: + print('no (Authentication failed: {})'.format(e)) + + except stem.connection: + print('no (Connection failed)') + + @staticmethod + def suggest(): + options = ['bandwidth', 'connections', 'countries', 'dormant', 'flags', 'routers', 'traffic'] + + for option in options: + print(option) + + def fetch(self): + raise NotImplementedError + + +########################## +# Child Classes +########################## + + +class TorBandwidth(TorPlugin): + def __init__(self): + pass + + def conf(self): + graph = {'title': 'Tor observed bandwidth', + 'args': '-l 0 --base 1000', + 'vlabel': 'bytes/s', + 'category': 'network', + 'info': 'estimated capacity based on usage in bytes/s'} + labels = {'bandwidth': {'label': 'bandwidth', 'min': 0, 'type': 'GAUGE'}} + + TorPlugin.conf_from_dict(graph, labels) + + def fetch(self): + with gen_controller() as controller: + try: + authenticate(controller) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + return + + # Get fingerprint of our own relay to look up the descriptor for. + # In Stem 1.3.0 and later, get_server_descriptor() will fetch the + # relay's own descriptor if no argument is provided, so this will + # no longer be needed. + fingerprint = controller.get_info('fingerprint', None) + if fingerprint is None: + print("Error while reading fingerprint from Tor daemon", file=sys.stderr) + sys.exit(1) + + response = controller.get_server_descriptor(fingerprint, None) + if response is None: + print("Error while getting server descriptor from Tor daemon", file=sys.stderr) + sys.exit(1) + print('bandwidth.value {}'.format(response.observed_bandwidth)) + + +class TorConnections(TorPlugin): + def __init__(self): + pass + + def conf(self): + graph = {'title': 'Tor connections', + 'args': '-l 0 --base 1000', + 'vlabel': 'connections', + 'category': 'network', + 'info': 'OR connections by state'} + labels = {'new': {'label': 'new', 'min': 0, 'max': 25000, 'type': 'GAUGE'}, + 'launched': {'label': 'launched', 'min': 0, 'max': 25000, 'type': 'GAUGE'}, + 'connected': {'label': 'connected', 'min': 0, 'max': 25000, 'type': 'GAUGE'}, + 'failed': {'label': 'failed', 'min': 0, 'max': 25000, 'type': 'GAUGE'}, + 'closed': {'label': 'closed', 'min': 0, 'max': 25000, 'type': 'GAUGE'}} + + TorPlugin.conf_from_dict(graph, labels) + + def fetch(self): + with gen_controller() as controller: + try: + authenticate(controller) + + response = controller.get_info('orconn-status', None) + if response is None: + print("No response from Tor daemon in TorConnection.fetch()", file=sys.stderr) + sys.exit(1) + else: + connections = response.split('\n') + states = dict((state, 0) for state in stem.ORStatus) + for connection in connections: + states[connection.rsplit(None, 1)[-1]] += 1 + for state, count in states.iteritems(): + print('{}.value {}'.format(state.lower(), count)) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + + +class TorCountries(TorPlugin): + def __init__(self): + # Configure plugin + self.cache_dir_name = os.environ.get('torcachedir', None) + if self.cache_dir_name is not None: + self.cache_dir_name = os.path.join(self.cache_dir_name, + os.environ.get('torcachefile', default_torcachefile)) + + max_countries = os.environ.get('tormaxcountries', default_tormaxcountries) + self.max_countries = int(max_countries) + + geoip_path = os.environ.get('torgeoippath', default_torgeoippath) + self.geodb = GeoIP.open(geoip_path, GeoIP.GEOIP_MEMORY_CACHE) + + def conf(self): + """Configure plugin""" + + graph = {'title': 'Tor countries', + 'args': '-l 0 --base 1000', + 'vlabel': 'countries', + 'category': 'network', + 'info': 'OR connections by state'} + labels = {} + + countries_num = self.top_countries() + + for c, v in countries_num: + labels[c] = {'label': c, 'min': 0, 'max': 25000, 'type': 'GAUGE'} + + TorPlugin.conf_from_dict(graph, labels) + + # If needed, create cache file at config time + if self.cache_dir_name: + with open(self.cache_dir_name, 'w') as f: + json.dump(countries_num, f) + + def fetch(self): + """Generate metrics""" + + # If possible, read cached data instead of doing the processing twice + try: + with open(self.cache_dir_name) as f: + countries_num = json.load(f) + except: + # Fallback if cache_dir_name is not set, unreadable or any other + # error + countries_num = self.top_countries() + + for c, v in countries_num: + print("%s.value %d" % (c, v)) + + @staticmethod + def _gen_ipaddrs_from_statuses(controller): + """Generate a sequence of ipaddrs for every network status""" + for desc in controller.get_network_statuses(): + ipaddr = desc.address + yield ipaddr + + @staticmethod + def simplify(cn): + """Simplify country name""" + cn = cn.replace(' ', '_') + cn = cn.replace("'", '_') + cn = cn.split(',', 1)[0] + return cn + + def _gen_countries(self, controller): + """Generate a sequence of countries for every built circuit""" + for ipaddr in self._gen_ipaddrs_from_statuses(controller): + country = self.geodb.country_name_by_addr(ipaddr) + if country is None: + yield 'Unknown' + continue + + yield self.simplify(country) + + def top_countries(self): + """Build a list of top countries by number of circuits""" + with gen_controller() as controller: + try: + authenticate(controller) + c = collections.Counter(self._gen_countries(controller)) + return sorted(c.most_common(self.max_countries)) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + return [] + + +class TorDormant(TorPlugin): + def __init__(self): + pass + + def conf(self): + graph = {'title': 'Tor dormant', + 'args': '-l 0 --base 1000', + 'vlabel': 'dormant', + 'category': 'network', + 'info': 'Is Tor not building circuits because it is idle?'} + labels = {'dormant': {'label': 'dormant', 'min': 0, 'max': 1, 'type': 'GAUGE'}} + + TorPlugin.conf_from_dict(graph, labels) + + def fetch(self): + with gen_controller() as controller: + try: + authenticate(controller) + + response = controller.get_info('dormant', None) + if response is None: + print("Error while reading dormant state from Tor daemon", file=sys.stderr) + sys.exit(1) + print('dormant.value {}'.format(response)) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + + +class TorFlags(TorPlugin): + def __init__(self): + pass + + def conf(self): + graph = {'title': 'Tor relay flags', + 'args': '-l 0 --base 1000', + 'vlabel': 'flags', + 'category': 'network', + 'info': 'Flags active for relay'} + labels = {flag: {'label': flag, 'min': 0, 'max': 1, 'type': 'GAUGE'} for flag in stem.Flag} + + TorPlugin.conf_from_dict(graph, labels) + + def fetch(self): + with gen_controller() as controller: + try: + authenticate(controller) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + return + + # Get fingerprint of our own relay to look up the status entry for. + # In Stem 1.3.0 and later, get_network_status() will fetch the + # relay's own status entry if no argument is provided, so this will + # no longer be needed. + fingerprint = controller.get_info('fingerprint', None) + if fingerprint is None: + print("Error while reading fingerprint from Tor daemon", file=sys.stderr) + sys.exit(1) + + response = controller.get_network_status(fingerprint, None) + if response is None: + print("Error while getting server descriptor from Tor daemon", file=sys.stderr) + sys.exit(1) + for flag in stem.Flag: + if flag in response.flags: + print('{}.value 1'.format(flag)) + else: + print('{}.value 0'.format(flag)) + + +class TorRouters(TorPlugin): + def __init__(self): + pass + + def conf(self): + graph = {'title': 'Tor routers', + 'args': '-l 0', + 'vlabel': 'routers', + 'category': 'network', + 'info': 'known Tor onion routers'} + labels = {'routers': {'label': 'routers', 'min': 0, 'type': 'GAUGE'} } + + + TorPlugin.conf_from_dict(graph, labels) + + def fetch(self): + with gen_controller() as controller: + try: + authenticate(controller) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + return + + + response = controller.get_info('ns/all', None) + if response is None: + print("Error while reading ns/all from Tor daemon", file=sys.stderr) + sys.exit(1) + else: + routers = response.split('\n') + onr = 0 + for router in routers: + if router[0] == "r": + onr += 1 + + print('routers.value {}'.format(onr)) + + +class TorTraffic(TorPlugin): + def __init__(self): + pass + + def conf(self): + graph = {'title': 'Tor traffic', + 'args': '-l 0 --base 1024', + 'vlabel': 'data', + 'category': 'network', + 'info': 'bytes read/written'} + labels = {'read': {'label': 'read', 'min': 0, 'type': 'DERIVE'}, + 'written': {'label': 'written', 'min': 0, 'type': 'DERIVE'}} + + TorPlugin.conf_from_dict(graph, labels) + + def fetch(self): + with gen_controller() as controller: + try: + authenticate(controller) + except stem.connection.AuthenticationFailure as e: + print('Authentication failed ({})'.format(e)) + return + + response = controller.get_info('traffic/read', None) + if response is None: + print("Error while reading traffic/read from Tor daemon", file=sys.stderr) + sys.exit(1) + + print('read.value {}'.format(response)) + + response = controller.get_info('traffic/written', None) + if response is None: + print("Error while reading traffic/write from Tor daemon", file=sys.stderr) + sys.exit(1) + print('written.value {}'.format(response)) + + +########################## +# Main +########################## + + +def main(): + if len(sys.argv) > 1: + param = sys.argv[1].lower() + else: + param = 'fetch' + + if param == 'autoconf': + TorPlugin.autoconf() + sys.exit() + elif param == 'suggest': + TorPlugin.suggest() + sys.exit() + else: + # detect data provider + if __file__.endswith('_bandwidth'): + provider = TorBandwidth() + elif __file__.endswith('_connections'): + provider = TorConnections() + elif __file__.endswith('_countries'): + provider = TorCountries() + elif __file__.endswith('_dormant'): + provider = TorDormant() + elif __file__.endswith('_flags'): + provider = TorFlags() + elif __file__.endswith('_routers'): + provider = TorRouters() + elif __file__.endswith('_traffic'): + provider = TorTraffic() + else: + print('Unknown plugin name, try "suggest" for a list of possible ones.', file=sys.stderr) + sys.exit(1) + + if param == 'config': + provider.conf() + elif param == 'fetch': + provider.fetch() + else: + print('Unknown parameter "{}"'.format(param), file=sys.stderr) + sys.exit(1) + +if __name__ == '__main__': + main()