diff --git a/active_check_traceroute.mkp b/active_check_traceroute.mkp new file mode 100644 index 0000000000000000000000000000000000000000..1fa711dfb8738d7caf5ba8e42dfd15a0da11910e Binary files /dev/null and b/active_check_traceroute.mkp differ diff --git a/checks/check_traceroute b/checks/check_traceroute new file mode 100644 index 0000000000000000000000000000000000000000..a0eec4bb41bdb64c7ef14129fe3acbb59f3cbf73 --- /dev/null +++ b/checks/check_traceroute @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + + +def check_traceroute_arguments(params): + args = [] + + if not params['dns']: + args.append('-n') + method = params['method'] + if method == 'icmp': + args.append('-I') + elif method == 'tcp': + args.append('-T') + # else: None -> default method + + for router, state in params['routers']: + args += ['-%s' % state, router] + + family = params.get('address_family') + if family is None: + family = 'ipv6' if is_ipv6_primary(host_name()) else 'ipv4' + + if family == 'ipv6': + args.append('-6') + else: + args.append('-4') + + # additional options + if 'port' in params: + args.append(f'-p {params["port"]}') + + if 'queries' in params: + args.append(f'-q {params["queries"]}') + + if 'max_ttl' in params: + args.append(f'-m {params["max_ttl"]}') + + # needs root + if 'source_interface' in params: + args.append(f'-i {params["source_interface"]}') + # needs root + if 'source_address' in params: + args.append(f'-s {params["source_address"]}') + + if 'destination_address' in params: + args.append(params['destination_address']) + else: + args.append('$HOSTADDRESS$') + + return args + + +def _check_description(params): + if 'description' in params: + return f'Routing {params["description"]}' + + return 'Routing' + + +active_check_info['traceroute'] = { + 'command_line': 'check_traceroute $ARG1$', + 'argument_function': check_traceroute_arguments, + 'service_description': _check_description, + 'has_perfdata': True, +} diff --git a/lib/nagios/plugins/check_traceroute b/lib/nagios/plugins/check_traceroute new file mode 100755 index 0000000000000000000000000000000000000000..dfa229014455cd29ba7a1d06fdf646c2b42bc4e7 --- /dev/null +++ b/lib/nagios/plugins/check_traceroute @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + +# This check does a traceroute to the specified target host +# (usually $HOSTADDRESS$ itself) and checks which route(s) are +# being taken. That way you can check if your preferred or +# some alternative route in in place. +# traceroute is expected to be in the search path and installed +# with SUID root bit. + +# Example output from traceroute -n +# traceroute to www.google.de (173.194.44.55), 30 hops max, 60 byte packets +# 1 10.10.11.4 0.419 ms 0.444 ms 0.518 ms +# 2 33.117.16.28 14.359 ms 14.371 ms 14.434 ms +# 3 112.18.7.119 14.750 ms 14.765 ms 19.530 ms +# 4 184.50.190.61 17.844 ms 17.865 ms 17.862 ms +# 5 67.249.94.88 24.285 ms 78.527 ms 26.834 ms +# 6 209.85.240.99 27.910 ms 27.420 ms 27.442 ms +# 7 173.194.44.55 26.583 ms 20.410 ms 23.142 ms + +# Output without -n option: +# traceroute to www.google.de (173.194.44.56), 30 hops max, 60 byte packets +# 1 fritz.box (10.10.11.4) 0.570 ms 0.606 ms 0.677 ms +# 2 foo-bar.x-online.net (33.117.16.28) 14.566 ms 14.580 ms 14.658 ms +# 3 xu-2-3-0.rt-inxs-1.x-online.net (112.13.6.109) 18.214 ms 18.228 ms 18.221 ms +# 4 * * * +# 5 66.249.94.88 (66.249.94.88) 24.481 ms 24.498 ms 24.271 ms +# 6 209.85.240.99 (209.85.240.99) 27.628 ms 21.605 ms 21.943 ms +# 7 muc03s08-in-f24.1e100.net (173.194.44.56) 21.277 ms 22.236 ms 22.192 ms + +# Example output for IPv6 +# traceroute to ipv6.google.com (2404:6800:4004:80e::200e), 30 hops max, 80 byte packets +# 1 2001:2e8:665:0:2:2:0:1 (2001:2e8:665:0:2:2:0:1) 0.082 ms 0.046 ms 0.044 ms +# 2 2001:2e8:22:204::2 (2001:2e8:22:204::2) 0.893 ms 0.881 ms 0.961 ms +# 3 * 2001:4860:0:1::1abd (2001:4860:0:1::1abd) 225.189 ms * +# 4 2001:4860:0:1003::1 (2001:4860:0:1003::1) 3.052 ms 2.820 ms 2001:4860:0:1002::1 (2001:4860:0:1002::1) 1.501 ms +# 5 nrt13s48-in-x0e.1e100.net (2404:6800:4004:80e::200e) 1.910 ms 1.828 ms 1.753 ms + +# It is also possible that for one hop several different answers appear: +# 11 xe-0-0-1-0.co2-96c-1b.ntwk.msn.net (204.152.141.11) 174.185 ms xe-10-0-2-0.co1-96c-1a.ntwk.msn.net (207.46.40.94) 174.279 ms xe-0-0-1-0.co2-96c-1b.ntwk.msn.net (204.152.141.11) 174.444 ms + +# if DNS fails then it looks like this: +# 5 66.249.94.88 (66.249.94.88) 24.481 ms 24.498 ms 24.271 ms +# 6 209.85.240.99 (209.85.240.99) 27.628 ms 21.605 ms 21.943 ms + +import ast +import getopt +import ipaddress +import os +import subprocess +import sys + + +class MissingValueError(Exception): + pass + + +class ProtocolVersionError(Exception): + pass + + +class ExecutionError(Exception): + pass + + +def parse_exception(exc): + exc = str(exc) + if exc[0] == "{": + exc = "%d - %s" % list(ast.literal_eval(exc).values())[0] + return str(exc) + + +def output_check_result(s, perfdata): + if perfdata: + perfdata_output_entries = ["%s=%s" % (p[0], ";".join(map(str, p[1:]))) for p in perfdata] + s += " | %s" % " ".join(perfdata_output_entries) + sys.stdout.write("%s\n" % s) + + +def option_to_state(c): + return {"w": 1, "c": 2}[c.lower()] + + +def _execute_traceroute(target, nodns, method, address_family, queries, max_ttl, port, source_addr, source_int): + cmd = ["traceroute"] + if nodns: + cmd.append("-n") + if method: + cmd.append(method) + if address_family: + cmd.append(address_family) + if port and method != "-I": + cmd.append(f"-p {port}") + if queries: + cmd.append(f"-q {queries}") + if max_ttl: + cmd.append(f"-m {max_ttl}") + if source_int: + cmd.append(f"-i {source_int}") + + if source_addr: + cmd.append(f"-s {source_addr}") + + cmd.append(target) + + if (source_int is not None) or (source_addr is not None): + cmd = ' '.join(cmd) + shell = True + else: + shell = False + + p = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf8", shell=shell) + sto, ste = p.communicate() + if p.returncode: + raise ExecutionError("UNKNOWN - " + ste.replace("\n", " ")) + return sto + + +def check_traceroute(lines, routes): + # find all visited routers + routers = set([]) + hops = len(lines[1:]) + for line in lines[1:]: + parts = line.strip().split() + for part in parts: + try: + part = part.lstrip("(").rstrip(",").rstrip(")") + ipaddress.ip_interface(part) + routers.add(part) + except ValueError: + pass + + state = 0 + bad_routers = [] + missing_routers = [] + for option, route in routes: + s = option_to_state(option) + if option.islower() and route in routers: + state = max(state, s) + bad_routers.append("%s(%s)" % (route, "!" * s)) + elif option.isupper() and route not in routers: + state = max(state, s) + missing_routers.append("%s(%s)" % (route, "!" * s)) + + info_text = "%d hops, missing routers: %s, bad routers: %s" % ( + hops, + missing_routers and ", ".join(missing_routers) or "none", + bad_routers and ", ".join(bad_routers) or "none", + ) + perfdata = [("hops", hops)] + return state, info_text, perfdata + + +def validate_ip_version(address_arg, ip_version_arg): + # ipv6 address can have an appended interface index/name: 'fe80::%{interface}' + try: + ip_address_version = ipaddress.ip_interface(address_arg.split("%")[0]).ip.version + except ValueError: + # May be a host or DNS name, don't execute the validation in this case. + # check_traceroute will perform the name resolution for us. + return + + if not ip_address_version == ip_version_arg: + raise ProtocolVersionError( + 'IP protocol version "%s" not the same as the IP address version "%s".' + % (ip_version_arg, ip_address_version) + ) + + +def usage(): + sys.stdout.write( + """check_traceroute -{c|w|C|W} ROUTE [-{o|c|w|O|C|W} ROUTE...] TARGET + +Check by which routes TARGET is being reached. Each possible route is being +prefixed with a state option: + + -w Make outcome WARN if that route is present + -W Make outcome WARN if that route is missing + -c Make outcome CRIT if that route is present + -C Make outcome CRIT if that route is missing + +Other options: + + -h, --help show this help and exit + --debug show Python exceptions verbosely + -n disable reverse DNS lookups + -I Use ICMP ECHO for probes + -T Use TCP SYN for probes + -4 Use IPv4 + -6 Use IPv6 + -p port Set the destination port to use + -m max_ttl Set the max number of hops (max TTL to be reached). Default is 30 + -s src_addr Use source address + -i device Specify a network interface to operate with + -q nqueries Set the number of probes per each hop. Default is 3 + +""" + ) + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + os.unsetenv("LANG") + + opt_verbose = 0 + opt_debug = False + opt_nodns = False + opt_method = None + opt_address_family = None + opt_port = None + opt_source_int = None + opt_source_addr = None + opt_queries = None + opt_max_ttl = None + + short_options = "hw:W:c:C:i:s:p:q:m:nTI46" + long_options = [ + "verbose", + "help", + "debug", + ] + + route_params = [] + + try: + opts, args = getopt.getopt(args, short_options, long_options) + + if len(args) < 1: + usage() + raise MissingValueError("Please specify the target destination.") + + target_address = args[0] + + # first parse modifers + for o, a in opts: + if o in ["-v", "--verbose"]: + opt_verbose += 1 + elif o in ["-d", "--debug"]: + opt_debug = True + elif o in ["-w", "-W", "-c", "-C"]: + route_params.append((o[1], a)) + elif o == "-n": + opt_nodns = True + elif o in ["-T", "-I"]: + opt_method = o + elif o in ["-4", "-6"]: + if opt_address_family: + raise ProtocolVersionError("Cannot use both IPv4 and IPv6") + validate_ip_version(target_address, int(o.lstrip("-"))) + opt_address_family = o + elif o in ["-s"]: + opt_source_addr = a + elif o in ["-i"]: + opt_source_int = a + elif o in ["-p"]: + opt_port = a + elif o in ["-q"]: + opt_queries = a + elif o in ["-m"]: + opt_max_ttl = a + + # now handle action options + for o, a in opts: + if o in ["-h", "--help"]: + usage() + sys.exit(0) + + sto = _execute_traceroute(target_address, opt_nodns, opt_method, opt_address_family, opt_queries, + opt_max_ttl, opt_port, opt_source_addr, opt_source_int) + status, output, perfdata = check_traceroute(sto.split("\n"), route_params) + info_text = output.strip() + "\n%s" % sto + return status, info_text, perfdata + + except ExecutionError as e: + return 3, str(e), None + + except MissingValueError as e: + return 3, str(e), None + + except ProtocolVersionError as e: + return 3, str(e), None + + except Exception as e: + if opt_debug: + raise + return 2, "Unhandled exception: %s" % parse_exception(e), None + + +if __name__ == "__main__": + exitcode, info, perf = main() + output_check_result(info, perf) + sys.exit(exitcode) diff --git a/packages/active_check_traceroute b/packages/active_check_traceroute new file mode 100644 index 0000000000000000000000000000000000000000..9d4a911cc4db8e1cfc323da20a529d914e981ac8 --- /dev/null +++ b/packages/active_check_traceroute @@ -0,0 +1,37 @@ +{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)', + 'description': 'extended traceroute check plugin\n' + '\n' + 'adds the following options:\n' + '- service description suffix\n' + '- alternate destination address\n' + '- destination port for UDP/TCP path trace\n' + '- max hops\n' + '- queries per hop\n' + '- source address \n' + '- source interface (needs root permissions).\n' + '\n' + 'Source address and source interface uses the "shell=True" ' + 'option in in\n' + 'the "subprocess.Popen" command. This is highly insecure, so ' + 'be careful.\n' + '\n' + 'Note: the original option TCP path trace also needs root ' + 'permissions.\n' + '\n' + 'To give "traceroute" root permissions you need to set the ' + 'SUID bit for\n' + 'your traceroute program, like "sudo chmod u+s ' + '/usr/bin/traceroute.db" \n' + 'on Ubuntu 20.04.3 LTS.\n', + 'download_url': 'https://thl-cmk.hopto.org', + 'files': {'checks': ['check_traceroute'], + 'lib': ['nagios/plugins/check_traceroute'], + 'web': ['plugins/wato/active_checks_routing.py', + 'plugins/metrics/traceroute.py']}, + 'name': 'active_check_traceroute', + 'num_files': 4, + 'title': 'Active Check Traceroute', + 'version': '20212104.v.0.0.1a', + 'version.min_required': '2.0.0', + 'version.packaged': '2021.09.20', + 'version.usable_until': None} \ No newline at end of file diff --git a/web/plugins/metrics/traceroute.py b/web/plugins/metrics/traceroute.py new file mode 100644 index 0000000000000000000000000000000000000000..9ee094f9109284f4267b8521ba36deaead3456d8 --- /dev/null +++ b/web/plugins/metrics/traceroute.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2021-12-04 +# +# +# + +from cmk.gui.i18n import _ + +from cmk.gui.plugins.metrics import ( + metric_info, + graph_info, + perfometer_info + +) + +metric_info['hops'] = { + 'title': _('Number of hops'), + 'unit': 'count', + 'color': '41/a', +} + +perfometer_info.append({ + 'type': 'linear', + 'segments': ['hops'], + 'total': 30, +}) diff --git a/web/plugins/wato/active_checks_routing.py b/web/plugins/wato/active_checks_routing.py new file mode 100644 index 0000000000000000000000000000000000000000..474e425250196def5f0ba3ab11432faf62982ad5 --- /dev/null +++ b/web/plugins/wato/active_checks_routing.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# +# (c) 2013 Heinlein Support GmbH +# Robert Sander <r.sander@heinlein-support.de> +# + +# This is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation in version 2. This file is distributed +# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with- +# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more de- +# ails. You should have received a copy of the GNU General Public +# License along with GNU Make; see the file COPYING. If not, write +# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301 USA. + +from cmk.gui.i18n import _ +from cmk.gui.valuespec import ( + Dictionary, + ListOf, + Tuple, + Transform, + Checkbox, + DropdownChoice, + TextInput, + Integer, + TextAscii, +) + +from cmk.gui.plugins.wato import ( + rulespec_registry, + HostRulespec, +) + +from cmk.gui.plugins.wato.active_checks import ( + RulespecGroupActiveChecks +) + + +def _ip_address_family_element(): + return ( + 'address_family', + DropdownChoice( + title=_('IP address family'), + choices=[ + (None, _('Primary address family')), + ('ipv4', _('Enforce IPv4')), + ('ipv6', _('Enforce IPv6')), + ], + default_value=None, + ), + ) + + +def _transform_add_address_family(v): + v.setdefault('address_family', None) + return v + + +def _valuespec_active_checks_traceroute(): + return Transform( + Dictionary( + title=_('Check current routing'), + help=_( + 'This active check uses <tt>traceroute</tt> in order to determine the current ' + 'routing from the monitoring host to the target host. You can specify any number ' + 'of missing or expected routes in order to detect e.g. an (unintended) failover ' + 'to a secondary route.' + ), + elements=[ + ('description', + TextAscii( + title=_('Service Description suffix'), + help=_('Must be unique for every host. The service description starts always with \"Routing\".'), + size=30, + )), + ('dns', + Checkbox( + title=_('Name resolution'), + label=_('Use DNS to convert IP addresses into hostnames'), + help=_( + 'If you use this option, then <tt>traceroute</tt> is <b>not</b> being ' + 'called with the option <tt>-n</tt>. That means that all IP addresses ' + 'are tried to be converted into names. This usually adds additional ' + 'execution time. Also DNS resolution might fail for some addresses.' + ), + ),), + _ip_address_family_element(), + ('routers', + ListOf( + Tuple( + elements=[ + TextInput( + title=_('Router (FQDN, IP-Address)'), + allow_empty=False, + ), + DropdownChoice( + title=_('How'), + choices=[ + ('W', _('WARN - if this router is not being used')), + ('C', _('CRIT - if this router is not being used')), + ('w', _('WARN - if this router is being used')), + ('c', _('CRIT - if this router is being used')), + ], + ), + ] + ), + title=_('Router that must or must not be used'), + add_label=_('Add Condition'), + ),), + ('method', + DropdownChoice( + title=_('Method of probing'), + choices=[ + (None, _('UDP (default behaviour of traceroute)')), + ('icmp', _('ICMP Echo Request')), + ('tcp', _('TCP SYN, needs root permissions')), + ], + default_value='icmp', + ),), + ('port', + Integer( + title=_('Port'), + help=_('Set the destination port to use. It is either initial udp port value for \"default\" ' + 'method (incremented by each probe, default is 33434), or initial seq for \"icmp\" ' + '(incremented as well, default from 1), or some constant destination port for other ' + 'methods (with default of 80 for \"tcp\", 53 for \"udp\", etc.)'), + maxvalue=65535, + ),), + ('destination_address', + TextAscii( + title=_('Alternate Destination'), + help=_('Path trace to alternate destination instead of \"HOSTADDRESS\".'), + ),), + ('queries', + Integer( + title=_('Number of queries'), + help=_('Set the number of probes per each hop. Default is 3.'), + default_value=3, + minvalue=1, + maxvalue=10, + ),), + ('max_ttl', + Integer( + title=_('Max hops'), + help=_('Set the max number of hops (max TTL to be reached). Default is 30'), + default_value=30, + minvalue=1, + maxvalue=255, + ),), + ('source_interface', + TextAscii( + title=_('Source interface'), + help=_('Specify a network interface to operate with. Needs root permissions.'), + ),), + ('source_address', + TextAscii( + title=_('Source address'), + help=_('Use source source address for outgoing packets'), + ),), + ], + optional_keys=['description', 'max_ttl', 'queries', 'destination_address', 'source_address', + 'source_interface', 'port'], + ), + forth=_transform_add_address_family, + ) + + +rulespec_registry.register( + HostRulespec( + group=RulespecGroupActiveChecks, + match_type='all', + name='active_checks:traceroute', + valuespec=_valuespec_active_checks_traceroute, + ) +)