#!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim: set fileencoding=utf-8:noet ## Copyright 2022 Bashclub ## BSD-2-Clause ## ## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: ## ## 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. ## ## 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. ## ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, ## THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS ## BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE ## GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ## LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## OPNsense CheckMK Agent ## to install ## copy to /usr/local/etc/rc.syshook.d/start/99-checkmk_agent and chmod +x ## __VERSION__ = "0.62" import sys import os import shlex import re import time import json import socket import signal import struct import subprocess import pwd import threading import ipaddress import base64 from cryptography import x509 from cryptography.hazmat.backends import default_backend as crypto_default_backend from xml.etree import cElementTree as ELementTree from collections import Counter,defaultdict from pprint import pprint from socketserver import TCPServer,StreamRequestHandler class object_dict(defaultdict): def __getattr__(self,name): return self[name] if name in self else "" def etree_to_dict(t): d = {t.tag: {} if t.attrib else None} children = list(t) if children: dd = object_dict(list) for dc in map(etree_to_dict, children): for k, v in dc.items(): dd[k].append(v) d = {t.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} if t.attrib: d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) if t.text: text = t.text.strip() if children or t.attrib: if text: d[t.tag]['#text'] = text else: d[t.tag] = text return d class checkmk_handler(StreamRequestHandler): def handle(self): with self.server._mutex: try: _strmsg = self.server.do_checks() except Exception as e: _strmsg = str(e) with self.wfile as _f: _f.write(_strmsg.encode("utf-8")) class checkmk_checker(object): _certificate_timestamp = 0 def do_checks(self): self._getosinfo() _errors = [] _lines = ["<<>>"] _lines.append("AgentOS: {os}".format(**self._info)) _lines.append(f"Version: {__VERSION__}") _lines.append("Hostname: {hostname}".format(**self._info)) for _check in dir(self): if _check.startswith("check_"): try: _lines += getattr(self,_check)() except Expetion as e: _errors.append(str(e)) _lines.append("<<>>") for _check in dir(self): if _check.startswith("checklocal_"): try: _lines += getattr(self,_check)() except Exeption as e: _errors.append(str(e)) _lines.append("") sys.stderr.write("\n".join(_errors)) sys.stderr.flush() return "\n".join(_lines) def _getosinfo(self): _info = json.load(open("/usr/local/opnsense/version/core","r")) _changelog = json.load(open("/usr/local/opnsense/changelog/index.json","r")) self._info = { "os" : _info.get("product_name"), "os_version" : _info.get("product_version"), "product_series" : _info.get("product_series"), "latest_version" : list(filter(lambda x: x.get("series") == _info.get("product_series"),_changelog))[-1].get("version"), "hostname" : self._run_prog("hostname").strip(" \n") } @staticmethod def ip2int(ipaddr): return struct.unpack("!I",socket.inet_aton(ipaddr))[0] @staticmethod def int2ip(intaddr): return socket.inet_ntoa(struct.pack("!I",intaddr)) def pidof(self,prog,default=None): _allprogs = re.findall("(\w+)\s+(\d+)",self._run_prog("ps ax -c -o command,pid")) return int(dict(_allprogs).get(prog,default)) def _config_reader(self,config=""): _config = ELementTree.parse("/conf/config.xml") _root = _config.getroot() return etree_to_dict(_root).get("opnsense",{}) @staticmethod def get_common_name(certrdn): try: return next(filter(lambda x: x.oid == x509.oid.NameOID.COMMON_NAME,certrdn)).value.strip() except: return str(certrdn) def _certificate_parser(self): self._certificate_timestamp = time.time() self._certificate_store = {} for _cert in self._config_reader().get("cert"): try: _certpem = base64.b64decode(_cert.get("crt")) _x509cert = x509.load_pem_x509_certificate(_certpem,crypto_default_backend()) _cert["not_valid_before"] = _x509cert.not_valid_before.timestamp() _cert["not_valid_after"] = _x509cert.not_valid_after.timestamp() _cert["serial"] = _x509cert.serial_number _cert["common_name"] = self.get_common_name(_x509cert.subject) _cert["issuer"] = self.get_common_name(_x509cert.issuer) except: pass self._certificate_store[_cert.get("refid")] = _cert def _get_certificate(self,refid): if time.time() - self._certificate_timestamp > 3600: self._certificate_parser() return self._certificate_store.get(refid) def _get_certificate_by_cn(self,cn,caref=None): if time.time() - self._certificate_timestamp > 3600: self._certificate_parser() if caref: _ret = filter(lambda x: x.get("common_name") == cn and x.get("caref") == caref,self._certificate_store.values()) else: _ret = filter(lambda x: x.get("common_name") == cn,self._certificate_store.values()) try: return next(_ret) except StopIteration: return {} def get_opnsense_interfaces(self): _ifs = {} #pprint(self._config_reader().get("interfaces")) #sys.exit(0) for _name,_interface in self._config_reader().get("interfaces",{}).items(): if _interface.get("enable") != "1": continue _desc = _interface.get("descr") _ifs[_interface.get("if","_")] = _desc if _desc else _name.upper() try: _wgserver = self._config_reader().get("OPNsense").get("wireguard").get("server").get("servers").get("server") if type(_wgserver) == dict: _wgserver = [_wgserver] _ifs.update( dict( map( lambda x: ("wg{}".format(x.get("instance")),"Wireguard_{}".format(x.get("name").strip().replace(" ","_"))), _wgserver ) ) ) except: pass return _ifs def checklocal_firmware(self): if self._info.get("os_version") != self._info.get("latest_version"): return ["1 Firmware update_available=1 Version {os_version} ({latest_version} available)".format(**self._info)] return ["0 Firmware update_available=0 Version {os_version}".format(**self._info)] def check_net(self): _opnsense_ifs = self.get_opnsense_interfaces() _mapdict = { "line rate" : ("speed", lambda x: int(int(x.split(" ")[0])/1000/1000)), "input errors" : ("ierror", lambda x: x), "output errors" : ("oerror", lambda x: x), "packets received" : ("ipackets", lambda x: x), "packets transmitted" : ("opackets", lambda x: x), "bytes received" : ("rx", lambda x: x), "bytes transmitted" : ("tx", lambda x: x), "collisions" : ("collissions", lambda x: x), } _now = int(time.time()) _ret = ["<<>>"] #_interface_status = dict(re.findall("^(\w+):.*?(UP|DOWN)",subprocess.check_output("ifconfig",encoding="utf-8"),re.M)) _interface_status = dict( map(lambda x: (x[0],(x[1:])), re.findall("^(?P\w+):.*?(?PUP|DOWN).*?\n(?:\s+(?:media:.*?(?P\d+).*?\<(?P.*?)\>|).*?\n)*", subprocess.check_output("ifconfig",encoding="utf-8"),re.M) ) ) _interface_data = self._run_prog("/usr/local/sbin/ifinfo") for _interface in re.finditer("^Interface\s(\w+).*?:\n((?:\s+\w+.*?\n)*)",_interface_data,re.M): _iface, _data = _interface.groups() _ifconfig = _interface_status.get(_iface,("","","")) _name = _opnsense_ifs.get(_iface) if not _name: continue _ifacedict = { "interface_name" : _name, "duplex" : _ifconfig[2] if _ifconfig[2] else "unknown", "systime" : _now, "up" : str(bool(_ifconfig[0] == "UP")).lower() } for _key,_val in re.findall("^\s+(.*?):\s(.*?)$",_data,re.M): _map = _mapdict.get(_key) if not _map: continue _ifacedict[_map[0]] = _map[1](_val) for _key,_val in _ifacedict.items(): _ret.append(f"{_iface}.{_key} {_val}") return _ret def check_dhcp(self): _ret = ["<<>>"] _ret.append("[general]\nPID: {0}".format(self.pidof("dhcpd",-1))) _dhcpleases = open("/var/dhcpd/var/db/dhcpd.leases","r").read() ## FIXME #_dhcpleases_dict = dict(map(lambda x: (self.ip2int(x[0]),x[1]),re.findall(r"lease\s(?P[0-9.]+)\s\{.*?.\n\s+binding state\s(?P\w+).*?\}",_dhcpleases,re.DOTALL))) _dhcpleases_dict = dict(re.findall(r"lease\s(?P[0-9.]+)\s\{.*?.\n\s+binding state\s(?Pactive).*?\}",_dhcpleases,re.DOTALL)) _dhcpconf = open("/var/dhcpd/etc/dhcpd.conf","r").read() _ret.append("[pools]") for _subnet in re.finditer(r"subnet\s(?P[0-9.]+)\snetmask\s(?P[0-9.]+)\s\{.*?(?:pool\s\{.*?\}.*?)*}",_dhcpconf,re.DOTALL): #_cidr = bin(self.ip2int(_subnet.group(2))).count("1") #_available = 0 for _pool in re.finditer("pool\s\{.*?range\s(?P[0-9.]+)\s(?P[0-9.]+).*?\}",_subnet.group(0),re.DOTALL): #_start,_end = self.ip2int(_pool.group(1)), self.ip2int(_pool.group(2)) #_ips_in_pool = filter(lambda x: _start < x[0] < _end,_dhcpleases_dict.items()) #pprint(_dhcpleases_dict) #pprint(sorted(list(map(lambda x: (self._int2ip(x[0]),x[1]),_ips_in_pool)))) #_available += (_end - _start) _ret.append("{0}\t{1}".format(_pool.group(1),_pool.group(2))) #_ret.append("DHCP_{0}/{1} {2}".format(_subnet.group(1),_cidr,_available)) _ret.append("[leases]") for _ip in sorted(_dhcpleases_dict.keys()): _ret.append(_ip) return _ret @staticmethod def _read_from_openvpnsocket(vpnsocket,cmd): _sock = socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) try: _sock.connect(vpnsocket) assert (_sock.recv(4096).decode("utf-8")).startswith(">INFO") cmd = cmd.strip() + "\n" _sock.send(cmd.encode("utf-8")) _data = "" while True: _socket_data = _sock.recv(4096).decode("utf-8") _data += _socket_data if _socket_data.strip().endswith("END") or _socket_data.strip().startswith("SUCCESS:"): break return _data finally: _sock.send("quit\n".encode("utf-8")) _sock.close() _sock = None return "" def checklocal_openvpn(self): _ret = [""] _cfr = self._config_reader().get("openvpn") if type(_cfr) != dict: return _ret _cso = _cfr.get("openvpn-csc") _monitored_clients = {} if type(_cso) == dict: _cso = [_cso] if type(_cso) == list: _monitored_clients = dict(map(lambda x: (x.get("common_name").upper(),dict(x,current=[])),_cso)) _now = time.time() _vpnserver = _cfr.get("openvpn-server",[]) if type(_vpnserver) == dict: _vpnserver = [_vpnserver] for _server in _vpnserver: _server["name"] = _server.get("description") if _server.get("description").strip() else "OpenVPN_{protocoll}_{local_port}".format(**_server) _caref = _server.get("caref") if not _server.get("maxclients"): _max_clients = ipaddress.IPv4Network(_server.get("tunnel_network")).num_addresses -2 if _server.get("topology_subnet") != "yes": _max_clients = max(1,int(_max_clients/4)) ## p2p _server["maxclients"] = _max_clients _server_cert = self._get_certificate(_server.get("certref")) _nclients, _server["bytesin"], _server["bytesout"] = 0,0,0 _server["expiredays"] = 0 _server["expiredate"] = "no certificate found" if _server_cert: _notvalidafter = _server_cert.get("not_valid_after") _server["expiredays"] = int((_notvalidafter - _now) / 86400) _server["expiredate"] = time.strftime("Cert Expire: %d.%m.%Y",time.localtime(_notvalidafter)) try: _unix = "/var/etc/openvpn/server{vpnid}.sock".format(**_server) try: _nclients, _server["bytesin"], _server["bytesout"] = re.findall("=(\d+)",self._read_from_openvpnsocket(_unix,"load-stats")) except: pass _number_of_clients = 0 _now = int(time.time()) _response = self._read_from_openvpnsocket(_unix,"status 2") for _client_match in re.finditer("^CLIENT_LIST,(.*?)$",_response,re.M): _number_of_clients += 1 _client_raw = _client_match.group(1).split(",") _client = { "server" : _server.get("name"), "common_name" : _client_raw[0], "remote_ip" : _client_raw[1].split(":")[0], "vpn_ip" : _client_raw[2], "vpn_ipv6" : _client_raw[3], "bytes_received" : int(_client_raw[4]), "bytes_sent" : int(_client_raw[5]), "uptime" : _now - int(_client_raw[7]), "username" : _client_raw[8] if _client_raw[8] != "UNDEF" else _client_raw[0], "clientid" : int(_client_raw[9]), "cipher" : _client_raw[11].strip("\r\n") } if _client_raw[0].upper() in _monitored_clients: _monitored_clients[_client_raw[0].upper()]["current"].append(_client) _server["status"] = 0 if _server["expiredays"] < 61: _server["status"] = 2 if _server["expiredays"] < 31 else 1 else: _server["expiredate"] = "\\n" + _server["expiredate"] _server["clientcount"] = _number_of_clients _ret.append('{status} "OpenVPN Server: {name}" connections_ssl_vpn={clientcount};;{maxclients}|if_in_octets={bytesin}|if_out_octets={bytesout}|expiredays={expiredays} {clientcount}/{maxclients} Connections Port:{local_port}/{protocol} {expiredate}'.format(**_server)) except: raise _server["status"] = 2 _ret.append('2 "OpenVPN Server: {name}" connections_ssl_vpn=0;;{maxclients}|expiredays={expiredays} Server down Port:{local_port}/{protocol} {expiredate}'.format(**_server)) for _client in _monitored_clients.values(): _current_conn = _client.get("current",[]) if not _client.get("description"): _client["description"] = _client.get("common_name") _client["description"] = _client["description"].strip(" \r\n") _client["expiredays"] = 0 _client["expiredate"] = "no certificate found" _client["status"] = 0 _cert = self._get_certificate_by_cn(_client.get("common_name")) if _cert: _notvalidafter = _cert.get("not_valid_after") _client["expiredays"] = int((_notvalidafter - _now) / 86400) _client["expiredate"] = time.strftime("Cert Expire: %d.%m.%Y",time.localtime(_notvalidafter)) if _client["expiredays"] < 61: _client["status"] = 2 if _client["expiredays"] < 31 else 1 else: _client["expiredate"] = "\\n" + _client["expiredate"] if _current_conn: _client["uptime"] = max(map(lambda x: x.get("uptime"),_current_conn)) _client["count"] = len(_current_conn) _client["bytes_received"] = sum(map(lambda x: x.get("bytes_received"),_current_conn)) _client["bytes_sent"] = sum(map(lambda x: x.get("bytes_sent"),_current_conn)) _client["longdescr"] = "" for _conn in _current_conn: _client["longdescr"] += "Server:{server} {remote_ip}->{vpn_ip} {cipher} ".format(**_conn) _ret.append('{status} "OpenVPN Client: {description}" connectiontime={uptime}|connections_ssl_vpn={count}|if_in_octets={bytes_received}|if_out_octets={bytes_sent}|expiredays={expiredays} {longdescr} {expiredate}'.format(**_client)) else: _ret.append('2 "OpenVPN Client: {description}" connectiontime=0|connections_ssl_vpn=0|if_in_octets=0|if_out_octets=0|expiredays={expiredays} Nicht verbunden {expiredate}'.format(**_client)) return _ret def check_df(self): _ret = ["<<>>"] _ret += self._run_prog("df -kTP -t ufs").split("\n")[1:] return _ret def check_zfs(self): _ret = ["<<>>"] _ret.append(self._run_prog("zfs get -t filesystem,volume -Hp name,quota,used,avail,mountpoint,type")) _ret.append("[df]") _ret.append(self._run_prog("df -kP -t zfs")) _ret.append("<<>>") _ret.append(self._run_prog("sysctl -q kstat.zfs.misc.arcstats").replace("kstat.zfs.misc.arcstats.","").strip()) return _ret def check_mounts(self): _ret = ["<<>>"] _ret.append(self._run_prog("mount -p -t ufs").strip()) return _ret def check_cpu(self): _ret = ["<<>>"] _loadavg = self._run_prog("sysctl -n vm.loadavg").strip("{} \n") _proc = self._run_prog("top -b -n 1").split("\n")[1].split(" ") _proc = "{0}/{1}".format(_proc[3],_proc[0]) _lastpid = self._run_prog("sysctl -n kern.lastpid").strip(" \n") _ncpu = self._run_prog("sysctl -n hw.ncpu").strip(" \n") _ret.append(f"{_loadavg} {_proc} {_lastpid} {_ncpu}") return _ret def check_netctr(self): _ret = ["<<>>"] _out = self._run_prog("netstat -inb") for _line in re.finditer("^(?!Name|lo|plip)(?P\w+)\s+(?P\d+).*?Link.*?\s+.*?\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)$",_out,re.M): _ret.append("{iface} {inbytes} {inpkts} {inerr} {indrop} 0 0 0 0 {outbytes} {outpkts} {outerr} 0 0 0 0 0".format(**_line.groupdict())) return _ret def check_ntp(self): _ret = ["<<>>"] for _line in self._run_prog("ntpq -np").split("\n")[2:]: if _line.strip(): _ret.append("{0} {1}".format(_line[0],_line[1:])) return _ret def check_tcp(self): _ret = ["<<>>"] _out = self._run_prog("netstat -na") counts = Counter(re.findall("ESTABLISHED|LISTEN",_out)) for _key,_val in counts.items(): _ret.append(f"{_key} {_val}") return _ret def check_ps(self): _ret = ["<<>>"] _out = self._run_prog("ps ax -o state,user,vsz,rss,pcpu,command") for _line in re.finditer("^(?P\w+)\s+(?P\w+)\s+(?P\d+)\s+(?P\d+)\s+(?P[\d.]+)\s+(?P.*)$",_out,re.M): _ret.append("({user},{vsz},{rss},{cpu}) {command}".format(**_line.groupdict())) return _ret def check_uptime(self): _ret = ["<<>>"] _uptime_sec = time.time() - int(self._run_prog("sysctl -n kern.boottime").split(" ")[3].strip(" ,")) _idle_sec = re.findall("(\d+):[\d.]+\s+\[idle\]",self._run_prog("ps axw"))[0] _ret.append(f"{_uptime_sec} {_idle_sec}") return _ret def _run_prog(self,cmdline="",*args,shell=False): if cmdline: args = list(args) + shlex.split(cmdline,posix=True) try: return subprocess.check_output(args,encoding="utf-8",shell=shell) except subprocess.CalledProcessError as e: return "" class checkmk_server(TCPServer,checkmk_checker): def __init__(self,port,pidfile,user,**kwargs): self.pidfile = pidfile self._mutex = threading.Lock() self.user = pwd.getpwnam(user) self.allow_reuse_address = True TCPServer.__init__(self,("",port),checkmk_handler,bind_and_activate=False) def _change_user(self): _, _, _uid, _gid, _, _, _ = self.user if os.getuid() != _uid: os.setgid(_gid) os.setuid(_uid) def server_start(self): sys.stderr.write("starting checkmk_agent\n") signal.signal(signal.SIGTERM, self._signal_handler) signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGHUP, self._signal_handler) sys.stderr.flush() self._change_user() try: self.server_bind() self.server_activate() except: self.server_close() raise try: self.serve_forever() except KeyboardInterrupt: sys.stdout.flush() sys.stdout.write("\n") pass def _signal_handler(self,signum,*args): if signum in (signal.SIGTERM,signal.SIGINT): sys.stderr.write("stopping checkmk_agent\n") threading.Thread(target=self.shutdown,name='shutdown').start() sys.exit(0) sys.stderr.write("checkmk_agent running\n") sys.stderr.flush() def daemonize(self): try: pid = os.fork() if pid > 0: ## first parent sys.exit(0) except OSError as e: print("Fork failed") sys.exit(1) os.chdir("/") os.setsid() os.umask(0) try: pid = os.fork() if pid > 0: ## second sys.exit(0) except OSError as e: print("Fork 2 failed") sys.exit(1) sys.stdout.flush() sys.stderr.flush() self._redirect_stream(sys.stdin,None) self._redirect_stream(sys.stdout,None) #self._redirect_stream(sys.stderr,None) with open(self.pidfile,"wt") as _pidfile: _pidfile.write(str(os.getpid())) os.chown(self.pidfile,self.user[2],self.user[3]) try: self.server_start() finally: try: os.remove(self.pidfile) except: pass @staticmethod def _redirect_stream(system_stream,target_stream): if target_stream is None: target_fd = os.open(os.devnull, os.O_RDWR) else: target_fd = target_stream.fileno() os.dup2(target_fd, system_stream.fileno()) def __del__(self): pass ## todo if __name__ == "__main__": import argparse _ = lambda x: x _parser = argparse.ArgumentParser(f"checkmk_agent for opnsense\nVersion: {__VERSION__}\n##########################################\n") _parser.add_argument("--port",type=int,default=6556, help=_("Port checkmk_agent listen")) _parser.add_argument("--start",action="store_true", help=_("")) _parser.add_argument("--stop",action="store_true", help=_("")) _parser.add_argument("--nodaemon",action="store_true", help=_("")) _parser.add_argument("--status",action="store_true", help=_("")) _parser.add_argument("--user",type=str,default="root", help=_("")) _parser.add_argument("--pidfile",type=str,default="/var/run/checkmk_agent.pid", help=_("")) _parser.add_argument("--debug",action="store_true", help=_("debug Ausgabe")) args = _parser.parse_args() _server = checkmk_server(**args.__dict__) _pid = None try: with open(args.pidfile,"rt") as _pidfile: _pid = int(_pidfile.read()) except (FileNotFoundError,IOError): _out = subprocess.check_output(["sockstat", "-l", "-p", str(args.port),"-P", "tcp"],encoding=sys.stdout.encoding) try: _pid = int(re.findall("\s(\d+)\s",_out.split("\n")[1])[0]) except (IndexError,ValueError): pass if args.start: if _pid: try: os.kill(_pid,0) except OSError: pass else: sys.stderr.write(f"allready running with pid {_pid}") sys.exit(1) _server.daemonize() elif args.status: if not _pid: sys.stderr.write("Not running\n") else: os.kill(int(_pid),signal.SIGHUP) elif args.stop: if not _pid: sys.stderr.write("Not running\n") sys.exit(1) os.kill(int(_pid),signal.SIGTERM) elif args.debug: print(_server.do_checks()) elif args.nodaemon: _server.server_start() else: # _server.server_start() ## default start daemon if _pid: try: os.kill(_pid,0) except OSError: pass else: sys.stderr.write(f"allready running with pid {_pid}") sys.exit(1) _server.daemonize()