2
0
mirror of https://github.com/munin-monitoring/contrib.git synced 2018-11-08 00:59:34 +01:00
contrib-munin/plugins/tor/tor_
Pierre-Alain TORET 452003a398 Update tor_ plugin author
Signed-off-by: Pierre-Alain TORET <pierre-alain.toret@protonmail.com>
2017-12-15 14:47:29 +01:00

540 lines
18 KiB
Python
Executable File

#!/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
Pierre-Alain TORET <pierre-alain.toret@protonmail.com>
'''
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()