From ab16e162ee224b4ac8933d6bb39421515ff6466b Mon Sep 17 00:00:00 2001 From: Michael Grote Date: Sun, 17 Oct 2021 13:34:18 +0200 Subject: [PATCH] =?UTF-8?q?tor=20plugin=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extern/tor_ | 556 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 extern/tor_ diff --git a/extern/tor_ b/extern/tor_ new file mode 100644 index 0000000..701e211 --- /dev/null +++ b/extern/tor_ @@ -0,0 +1,556 @@ +#!/usr/bin/env python3 +''' +=head1 NAME + +tor_ + +=head1 DESCRIPTION + +Wildcard plugin that gathers some metrics from the Tor daemon +(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: + +=over 4 + +=item tor_bandwidth - graph the glabal bandwidth + +=item tor_connections - graph the number of connexions + +=item tor_countries - graph the countries represented our connexions + +=item tor_dormant - graph if tor is dormant or not + +=item tor_flags - graph the different flags of the relay + +=item tor_routers - graph the number of routers seen by the relay + +=item tor_traffic - graph the read/written traffic + +=back + +=head2 CONFIGURATION + +The default configuration is: + + [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, you simply need to change C: + + env.torconnectmethod socket + +=head1 COPYRIGHT + +MIT License + +SPDX-License-Identifier: MIT + +=head1 AUTHOR + +Pierre-Alain TORET + +=head1 MAGIC MARKERS + + #%# family=auto + #%# capabilities=autoconf suggest + +=cut +''' + +import collections +import json +import os +import sys + +try: + import GeoIP + import stem + import stem.control + import stem.connection + missing_dependency_error = None +except ImportError as exc: + # missing dependencies are reported via "autoconf" + # thus failure is acceptable here + missing_dependency_error = str(exc) + +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' + + +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.items(): + print('graph_{} {}'.format(key, val)) + # values + for label, attributes in labels.items(): + for key, val in attributes.items(): + print('{}.{} {}'.format(label, key, val)) + + @staticmethod + def get_autoconf_status(): + try: + import stem + except ImportError as e: + return 'no (failed to import the required python module "stem": {})'.format(e) + try: + import GeoIP # noqa: F401 + except ImportError as e: + return 'no (failed to import the required python module "GeoIP": {})'.format(e) + try: + with gen_controller() as controller: + try: + authenticate(controller) + return 'yes' + except stem.connection.AuthenticationFailure as e: + return 'no (Authentication failed: {})'.format(e) + except stem.SocketError: + return '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.items(): + 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""" + # Fallback if cache_dir_name is not set, unreadable or any other error + countries_num = self.top_countries() + # If possible, read cached data instead of doing the processing twice + if self.cache_dir_name: + try: + with open(self.cache_dir_name) as f: + countries_num = json.load(f) + except (IOError, ValueError): + # use the fallback value above + pass + + 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': 'bytes/s', + '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': + print(TorPlugin.get_autoconf_status()) + sys.exit() + elif param == 'suggest': + TorPlugin.suggest() + sys.exit() + else: + if missing_dependency_error is not None: + print("Failed to run tor_ due to missing dependency: {}" + .format(missing_dependency_error), file=sys.stderr) + sys.exit(1) + # 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()