mirror of
https://github.com/librenms/librenms-agent.git
synced 2024-05-09 09:54:52 +00:00
254 lines
9.4 KiB
Python
Executable File
254 lines
9.4 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Name: Wireguard Script
|
|
# Author: bnerickson <bnerickson87@gmail.com> w/SourceDoctor's certificate.py script forming the
|
|
# base of the vast majority of this one.
|
|
# Version: 1.0
|
|
# Description: This is a simple script to parse "wg show all" output for ingestion into LibreNMS
|
|
# via the wireguard application. We collect traffic, a friendly identifier (arbitrary
|
|
# name), and last handshake time for all clients on all wireguard interfaces.
|
|
# Installation:
|
|
# 1. Copy this script to /etc/snmp/ and make it executable:
|
|
# chmod +x /etc/snmp/wireguard.py
|
|
# 2. Edit your snmpd.conf and include:
|
|
# extend wireguard /etc/snmp/wireguard.py
|
|
# 3. Create a /etc/snmp/wireguard.json file and specify:
|
|
# a.) (optional) "wg_cmd" - String path to the wg binary ["/usr/bin/wg"]
|
|
# b.) "public_key_to_arbitrary_name" - Two nested dictionaries where the key for the outer
|
|
# dictionary is the interface name, and the value for
|
|
# the outer dictionary is the inner dictionary. The
|
|
# inner dictionary is composed of key values
|
|
# corresponding to the clients' public keys
|
|
# (specified in the wireguard interface config file)
|
|
# and values corresponding to arbitrary friendly
|
|
# names. The friendly names MUST be unique within
|
|
# each interface. Also note that the interface name
|
|
# and friendly names are used in the RRD filename,
|
|
# so using special characters is highly discouraged.
|
|
# ```
|
|
# {
|
|
# "wg_cmd": "/bin/wg",
|
|
# "public_key_to_arbitrary_name": {
|
|
# "wg0": {
|
|
# "z1iSIymFEFi/PS8rR19AFBle7O4tWowMWuFzHO7oRlE=": "client1",
|
|
# "XqWJRE21Fw1ke47mH1yPg/lyWqCCfjkIXiS6JobuhTI=": "server.domain.com"
|
|
# }
|
|
# }
|
|
# }
|
|
# ```
|
|
# 4. Restart snmpd and activate the app for desired host.
|
|
# TODO:
|
|
# 1. If Wireguard ever implements a friendly identifier, then scrape that instead of providing
|
|
# arbitrary names manually in the json conf file.
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
from itertools import chain
|
|
|
|
CONFIG_FILE = "/etc/snmp/wireguard.json"
|
|
WG_CMD = "/usr/bin/wg"
|
|
WG_ARGS_SHOW_INTFS = ["show", "interfaces"]
|
|
|
|
|
|
def error_handler(error_name, err):
|
|
"""
|
|
error_handler(): Common error handler for config/output parsing and command execution. We set
|
|
the data to none and print out the json.
|
|
Inputs:
|
|
error_name: String describing the error handled.
|
|
err: The error message in its entirety.
|
|
Outputs:
|
|
None
|
|
"""
|
|
output_data = {
|
|
"errorString": "%s: '%s'" % (error_name, err),
|
|
"error": 1,
|
|
"version": 1,
|
|
"data": {},
|
|
}
|
|
print(json.dumps(output_data))
|
|
sys.exit(1)
|
|
|
|
|
|
def config_file_parser():
|
|
"""
|
|
config_file_parser(): Parse the config file and extract the necessary parameters.
|
|
|
|
Inputs:
|
|
None
|
|
Outputs:
|
|
wg_cmd: The final wg binary to execute.
|
|
interface_clients_dict: Dictionary mapping of interface names to public_key->client names.
|
|
"""
|
|
# Load configuration file if it exists
|
|
try:
|
|
with open(CONFIG_FILE, "r") as json_file:
|
|
config_file = json.load(json_file)
|
|
interface_clients_dict = config_file["public_key_to_arbitrary_name"]
|
|
wg_cmd = [config_file["wg_cmd"]] if "wg_cmd" in config_file else [WG_CMD]
|
|
except (
|
|
FileNotFoundError,
|
|
KeyError,
|
|
PermissionError,
|
|
OSError,
|
|
json.decoder.JSONDecodeError,
|
|
) as err:
|
|
error_handler("Config File Error", err)
|
|
|
|
return wg_cmd, interface_clients_dict
|
|
|
|
|
|
def config_file_validator(interface_clients_dict):
|
|
"""
|
|
config_file_validator(): Verifies the uniqueness of the arbitrary names in the interface to
|
|
public_key->client names dictionary.
|
|
|
|
Inputs:
|
|
interface_clients_dict: Dictionary mapping of interface names to public_key->client names.
|
|
Outputs:
|
|
None
|
|
"""
|
|
# Search for valid, unique arbitrary names
|
|
for interface, public_key_to_arbitrary_name in interface_clients_dict.items():
|
|
rev_dict = {}
|
|
for public_key, arbitrary_name in public_key_to_arbitrary_name.items():
|
|
rev_dict.setdefault(str(arbitrary_name), set()).add(public_key)
|
|
|
|
# Verify the arbitrary names set in the wireguard.json file are unique.
|
|
result = set(
|
|
chain.from_iterable(
|
|
arbitrary_name
|
|
for public_key, arbitrary_name in rev_dict.items()
|
|
if len(arbitrary_name) > 1
|
|
)
|
|
)
|
|
if not result:
|
|
continue
|
|
|
|
err = (
|
|
"%s interface has non-unique arbitrary names configured for public keys %s"
|
|
% (interface, str(result))
|
|
)
|
|
error_handler("Config File Error", err)
|
|
|
|
|
|
def command_executor(wg_cmd_full):
|
|
"""
|
|
command_executor(): Execute the wg command and return the output.
|
|
|
|
Inputs:
|
|
wg_cmd_full: The full wg command to execute.
|
|
Outputs:
|
|
poutput: The stdout of the executed command (empty byte-string if error).
|
|
"""
|
|
try:
|
|
# Execute wg command
|
|
poutput = subprocess.check_output(
|
|
wg_cmd_full,
|
|
stdin=None,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
except (subprocess.CalledProcessError, OSError) as err:
|
|
error_handler("Command Execution Error", err)
|
|
return poutput
|
|
|
|
|
|
def output_parser(line, interface_clients_dict, interface):
|
|
"""
|
|
output_parser(): Parses a line from the wg command for the client's public key, traffic inbound
|
|
and outbound, wireguard interface, and last handshake timestamp.
|
|
|
|
Inputs:
|
|
line: The wireguard client status line from the wg command stdout.
|
|
interface_clients_dict: Dictionary mapping of interface to public_key->client names.
|
|
interface: The wireguard interface we are parsing.
|
|
Outputs:
|
|
wireguard_data: A dictionary of a peer's public key, bytes sent and received, and minutes
|
|
since last handshake.
|
|
"""
|
|
line_parsed = line.strip().split()
|
|
|
|
try:
|
|
public_key = str(line_parsed[0])
|
|
timestamp = int(line_parsed[4])
|
|
bytes_rcvd = int(line_parsed[5])
|
|
bytes_sent = int(line_parsed[6])
|
|
except (IndexError, ValueError) as err:
|
|
error_handler("Command Output Parsing Error", err)
|
|
|
|
# Return an empty dictionary if the interface is not in the dictionary.
|
|
if interface not in interface_clients_dict:
|
|
return {}
|
|
|
|
# Return an empty dictionary if there is no public key to arbitrary name mapping.
|
|
if public_key not in interface_clients_dict[interface]:
|
|
return {}
|
|
|
|
# Perform in-place replacement of publickeys with arbitrary names.
|
|
friendly_name = str(interface_clients_dict[interface][public_key])
|
|
|
|
# Calculate minutes since last handshake here
|
|
last_handshake_timestamp = datetime.fromtimestamp(timestamp) if timestamp else 0
|
|
minutes_since_last_handshake = (
|
|
int((datetime.now() - last_handshake_timestamp).total_seconds() / 60)
|
|
if last_handshake_timestamp
|
|
else None
|
|
)
|
|
|
|
wireguard_data = {
|
|
friendly_name: {
|
|
"minutes_since_last_handshake": minutes_since_last_handshake,
|
|
"bytes_rcvd": bytes_rcvd,
|
|
"bytes_sent": bytes_sent,
|
|
}
|
|
}
|
|
|
|
return wireguard_data
|
|
|
|
|
|
def main():
|
|
"""
|
|
main(): main function that delegates config file parsing, command execution, and unit stdout
|
|
parsing. Then it prints out the expected json output for the wireguard application.
|
|
|
|
Inputs:
|
|
None
|
|
Outputs:
|
|
None
|
|
"""
|
|
output_data = {"errorString": "", "error": 0, "version": 1, "data": {}}
|
|
|
|
# Parse configuration file.
|
|
wg_cmd, interface_clients_dict = config_file_parser()
|
|
|
|
# Verify contents of the config file are valid.
|
|
config_file_validator(interface_clients_dict)
|
|
|
|
# Get list of interfaces
|
|
wg_cmd_show_intfs = wg_cmd + WG_ARGS_SHOW_INTFS
|
|
wg_intfs = command_executor(wg_cmd_show_intfs).decode("utf-8").strip().split(" ")
|
|
|
|
# Execute wg command on each discovered interface and parse output. We skip the first line
|
|
# ("[1:]") since that's the wireguard server's public key declaration.
|
|
for interface in wg_intfs:
|
|
wg_cmd_dump = wg_cmd + ["show"] + [interface] + ["dump"]
|
|
output_data["data"][interface] = {}
|
|
for line in command_executor(wg_cmd_dump).decode("utf-8").split("\n")[1:]:
|
|
if not line:
|
|
continue
|
|
# Parse each line and import the resultant dictionary into output_data. We update the
|
|
# interface key with new clients as they are found and instantiate new interface keys as
|
|
# they are found.
|
|
for friendly_name, client_data in output_parser(
|
|
line, interface_clients_dict, interface
|
|
).items():
|
|
output_data["data"][interface][friendly_name] = client_data
|
|
|
|
print(json.dumps(output_data))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|