diff options
-rw-r--r-- | op-mode-definitions/generate-openvpn-config-client.xml.in | 58 | ||||
-rw-r--r-- | python/vyos/configtree.py | 65 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_monitoring_telegraf.py | 2 | ||||
-rwxr-xr-x | src/op_mode/generate_ovpn_client_file.py | 145 | ||||
-rwxr-xr-x | src/op_mode/show_ipsec_sa.py | 186 |
5 files changed, 348 insertions, 108 deletions
diff --git a/op-mode-definitions/generate-openvpn-config-client.xml.in b/op-mode-definitions/generate-openvpn-config-client.xml.in new file mode 100644 index 000000000..4f9f31bfe --- /dev/null +++ b/op-mode-definitions/generate-openvpn-config-client.xml.in @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="openvpn"> + <properties> + <help>Generate OpenVPN client configuration ovpn file</help> + </properties> + <children> + <node name="client-config"> + <properties> + <help>Generate Client config</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Local interface used for connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type openvpn</script> + </completionHelp> + </properties> + <children> + <tagNode name="ca"> + <properties> + <help>CA certificate</help> + <completionHelp> + <path>pki ca</path> + </completionHelp> + </properties> + <children> + <tagNode name="certificate"> + <properties> + <help>Cerificate used by client</help> + <completionHelp> + <path>pki certificate</path> + </completionHelp> + </properties> + <children> + <tagNode name="key"> + <properties> + <help>Certificate key used by client</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_ovpn_client_file.py --interface "$5" --ca "$7" --cert "$9" --key "${11}"</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/generate_ovpn_client_file.py --interface "$5" --ca "$7" --cert "$9"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index d8ffaca99..866f24e47 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -15,8 +15,9 @@ import re import json -from ctypes import cdll, c_char_p, c_void_p, c_int +from ctypes import cdll, c_char_p, c_void_p, c_int, POINTER +LIBPATH = '/usr/lib/libvyosconfig.so.0' def escape_backslash(string: str) -> str: """Escape single backslashes in string that are not in escape sequence""" @@ -42,7 +43,9 @@ class ConfigTreeError(Exception): class ConfigTree(object): - def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'): + def __init__(self, config_string=None, address=None, libpath=LIBPATH): + if config_string is None and address is None: + raise TypeError("ConfigTree() requires one of 'config_string' or 'address'") self.__config = None self.__lib = cdll.LoadLibrary(libpath) @@ -60,7 +63,7 @@ class ConfigTree(object): self.__to_string.restype = c_char_p self.__to_commands = self.__lib.to_commands - self.__to_commands.argtypes = [c_void_p] + self.__to_commands.argtypes = [c_void_p, c_char_p] self.__to_commands.restype = c_char_p self.__to_json = self.__lib.to_json @@ -126,15 +129,19 @@ class ConfigTree(object): self.__destroy = self.__lib.destroy self.__destroy.argtypes = [c_void_p] - config_section, version_section = extract_version(config_string) - config_section = escape_backslash(config_section) - config = self.__from_string(config_section.encode()) - if config is None: - msg = self.__get_error().decode() - raise ValueError("Failed to parse config: {0}".format(msg)) + if address is None: + config_section, version_section = extract_version(config_string) + config_section = escape_backslash(config_section) + config = self.__from_string(config_section.encode()) + if config is None: + msg = self.__get_error().decode() + raise ValueError("Failed to parse config: {0}".format(msg)) + else: + self.__config = config + self.__version = version_section else: - self.__config = config - self.__version = version_section + self.__config = address + self.__version = '' def __del__(self): if self.__config is not None: @@ -143,13 +150,16 @@ class ConfigTree(object): def __str__(self): return self.to_string() + def _get_config(self): + return self.__config + def to_string(self): config_string = self.__to_string(self.__config).decode() config_string = "{0}\n{1}".format(config_string, self.__version) return config_string - def to_commands(self): - return self.__to_commands(self.__config).decode() + def to_commands(self, op="set"): + return self.__to_commands(self.__config, op.encode()).decode() def to_json(self): return self.__to_json(self.__config).decode() @@ -281,3 +291,32 @@ class ConfigTree(object): else: raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) +class Diff: + def __init__(self, left, right, path=[], libpath=LIBPATH): + if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): + raise TypeError("Arguments must be instances of ConfigTree") + if path: + if not left.exists(path): + raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree") + if not right.exists(path): + raise ConfigTreeError(f"Path {path} doesn't exist in rhs tree") + self.left = left + self.right = right + + check_path(path) + path_str = " ".join(map(str, path)).encode() + df = cdll.LoadLibrary(libpath).diffs + df.restype = POINTER(c_void_p * 3) + res = list(df(path_str, left._get_config(), right._get_config()).contents) + self._diff = {'add': ConfigTree(address=res[0]), + 'del': ConfigTree(address=res[1]), + 'int': ConfigTree(address=res[2]) } + + self.add = self._diff['add'] + self.delete = self._diff['del'] + self.inter = self._diff['int'] + + def to_commands(self): + add = self.add.to_commands() + delete = self.delete.to_commands(op="delete") + return delete + "\n" + add diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py index b857926e2..09937513e 100755 --- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -54,7 +54,7 @@ class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase): # Check telegraf config self.assertIn(f'organization = "{org}"', config) - self.assertIn(token, config) + self.assertIn(f' token = "$INFLUX_TOKEN"', config) self.assertIn(f'urls = ["{url}:{port}"]', config) self.assertIn(f'bucket = "{bucket}"', config) diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py new file mode 100755 index 000000000..29db41e37 --- /dev/null +++ b/src/op_mode/generate_ovpn_client_file.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os + +from jinja2 import Template + +from vyos.configquery import ConfigTreeQuery +from vyos.ifconfig import Section +from vyos.util import cmd + + +client_config = """ + +client +nobind +remote {{ remote_host }} {{ port }} +remote-cert-tls server +proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }} +dev {{ device }} +dev-type {{ device }} +persist-key +persist-tun +verb 3 + +# Encryption options +{% if encryption is defined and encryption is not none %} +{% if encryption.cipher is defined and encryption.cipher is not none %} +cipher {{ encryption.cipher }} +{% if encryption.cipher == 'bf128' %} +keysize 128 +{% elif encryption.cipher == 'bf256' %} +keysize 256 +{% endif %} +{% endif %} +{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %} +data-ciphers {{ encryption.ncp_ciphers }} +{% endif %} +{% endif %} + +{% if hash is defined and hash is not none %} +auth {{ hash }} +{% endif %} +keysize 256 +comp-lzo {{ '' if use_lzo_compression is defined else 'no' }} + +<ca> +-----BEGIN CERTIFICATE----- +{{ ca }} +-----END CERTIFICATE----- + +</ca> + +<cert> +-----BEGIN CERTIFICATE----- +{{ cert }} +-----END CERTIFICATE----- + +</cert> + +<key> +-----BEGIN PRIVATE KEY----- +{{ key }} +-----END PRIVATE KEY----- + +</key> + +""" + +config = ConfigTreeQuery() +base = ['interfaces', 'openvpn'] + +if not config.exists(base): + print('OpenVPN not configured') + exit(0) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True) + parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True) + parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True) + parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store") + args = parser.parse_args() + + interface = args.interface + ca = args.ca + cert = args.cert + key = args.key + if not key: + key = args.cert + + if interface not in Section.interfaces('openvpn'): + exit(f'OpenVPN interface "{interface}" does not exist!') + + if not config.exists(['pki', 'ca', ca, 'certificate']): + exit(f'OpenVPN CA certificate "{ca}" does not exist!') + + if not config.exists(['pki', 'certificate', cert, 'certificate']): + exit(f'OpenVPN certificate "{cert}" does not exist!') + + if not config.exists(['pki', 'certificate', cert, 'private', 'key']): + exit(f'OpenVPN certificate key "{key}" does not exist!') + + ca = config.value(['pki', 'ca', ca, 'certificate']) + cert = config.value(['pki', 'certificate', cert, 'certificate']) + key = config.value(['pki', 'certificate', key, 'private', 'key']) + remote_host = config.value(base + [interface, 'local-host']) + + ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True) + + port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port'] + proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol'] + device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type'] + + config = { + 'interface' : interface, + 'ca' : ca, + 'cert' : cert, + 'key' : key, + 'device' : device, + 'port' : port, + 'proto' : proto, + 'remote_host' : remote_host, + 'address' : [], + } + +# Clear out terminal first +print('\x1b[2J\x1b[H') +client = Template(client_config, trim_blocks=True).render(config) +print(client) diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py index e72f0f965..5b8f00dba 100755 --- a/src/op_mode/show_ipsec_sa.py +++ b/src/op_mode/show_ipsec_sa.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,119 +14,117 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import re -import sys +from re import split as re_split +from sys import exit -import vici -import tabulate -import hurry.filesize +from hurry import filesize +from tabulate import tabulate +from vici import Session as vici_session + +from vyos.util import seconds_to_human -import vyos.util def convert(text): return int(text) if text.isdigit() else text.lower() + def alphanum_key(key): - return [convert(c) for c in re.split('([0-9]+)', str(key))] + return [convert(c) for c in re_split('([0-9]+)', str(key))] -def format_output(conns, sas): + +def format_output(sas): sa_data = [] - for peer, parent_conn in conns.items(): - if peer not in sas: - continue - - parent_sa = sas[peer] - child_sas = parent_sa['child-sas'] - installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} - - # parent_sa["state"] = IKE state, child_sas["state"] = ESP state - state = 'down' - uptime = 'N/A' - - if parent_sa["state"] == b"ESTABLISHED" and installed_sas: - state = "up" - - remote_host = parent_sa["remote-host"].decode() - remote_id = parent_sa["remote-id"].decode() - - if remote_host == remote_id: - remote_id = "N/A" - - # The counters can only be obtained from the child SAs - for child_conn in parent_conn['children']: - if child_conn not in installed_sas: - data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] - sa_data.append(data) - continue - - isa = installed_sas[child_conn] - csa_name = isa['name'] - csa_name = csa_name.decode() - - bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) - bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) - bytes_str = "{0}/{1}".format(bytes_in, bytes_out) - - pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) - pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) - pkts_str = "{0}/{1}".format(pkts_in, pkts_out) - # Remove B from <1K values - pkts_str = re.sub(r'B', r'', pkts_str) - - uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) - - enc = isa["encr-alg"].decode() - if "encr-keysize" in isa: - key_size = isa["encr-keysize"].decode() - else: - key_size = "" - if "integ-alg" in isa: - hash = isa["integ-alg"].decode() - else: - hash = "" - if "dh-group" in isa: - dh_group = isa["dh-group"].decode() - else: - dh_group = "" - - proposal = enc - if key_size: - proposal = "{0}_{1}".format(proposal, key_size) - if hash: - proposal = "{0}/{1}".format(proposal, hash) - if dh_group: - proposal = "{0}/{1}".format(proposal, dh_group) - - data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] - sa_data.append(data) + for sa in sas: + for parent_sa in sa.values(): + # create an item for each child-sa + for child_sa in parent_sa.get('child-sas', {}).values(): + # prepare a list for output data + sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A' + + # collect raw data + sa_name = child_sa.get('name') + sa_state = child_sa.get('state') + sa_uptime = child_sa.get('install-time') + sa_bytes_in = child_sa.get('bytes-in') + sa_bytes_out = child_sa.get('bytes-out') + sa_packets_in = child_sa.get('packets-in') + sa_packets_out = child_sa.get('packets-out') + sa_remote_addr = parent_sa.get('remote-host') + sa_remote_id = parent_sa.get('remote-id') + sa_proposal_encr_alg = child_sa.get('encr-alg') + sa_proposal_integ_alg = child_sa.get('integ-alg') + sa_proposal_encr_keysize = child_sa.get('encr-keysize') + sa_proposal_dh_group = child_sa.get('dh-group') + + # format data to display + if sa_name: + sa_out_name = sa_name.decode() + if sa_state: + if sa_state == b'INSTALLED': + sa_out_state = 'up' + else: + sa_out_state = 'down' + if sa_uptime: + sa_out_uptime = seconds_to_human(sa_uptime.decode()) + if sa_bytes_in and sa_bytes_out: + bytes_in = filesize.size(int(sa_bytes_in.decode())) + bytes_out = filesize.size(int(sa_bytes_out.decode())) + sa_out_bytes = f'{bytes_in}/{bytes_out}' + if sa_packets_in and sa_packets_out: + packets_in = filesize.size(int(sa_packets_in.decode()), + system=filesize.si) + packets_out = filesize.size(int(sa_packets_out.decode()), + system=filesize.si) + sa_out_packets = f'{packets_in}/{packets_out}' + if sa_remote_addr: + sa_out_remote_addr = sa_remote_addr.decode() + if sa_remote_id: + sa_out_remote_id = sa_remote_id.decode() + # format proposal + if sa_proposal_encr_alg: + sa_out_proposal = sa_proposal_encr_alg.decode() + if sa_proposal_encr_keysize: + sa_proposal_encr_keysize_str = sa_proposal_encr_keysize.decode() + sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}' + if sa_proposal_integ_alg: + sa_proposal_integ_alg_str = sa_proposal_integ_alg.decode() + sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}' + if sa_proposal_dh_group: + sa_proposal_dh_group_str = sa_proposal_dh_group.decode() + sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}' + + # add a new item to output data + sa_data.append([ + sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes, + sa_out_packets, sa_out_remote_addr, sa_out_remote_id, + sa_out_proposal + ]) + + # return output data return sa_data + if __name__ == '__main__': try: - session = vici.Session() - conns = {} - sas = {} + session = vici_session() + sas = list(session.list_sas()) - for conn in session.list_conns(): - for key in conn: - conns[key] = conn[key] - - for sa in session.list_sas(): - for key in sa: - sas[key] = sa[key] - - headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] - sa_data = format_output(conns, sas) + sa_data = format_output(sas) sa_data = sorted(sa_data, key=alphanum_key) - output = tabulate.tabulate(sa_data, headers) + + headers = [ + "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", + "Remote address", "Remote ID", "Proposal" + ] + output = tabulate(sa_data, headers) print(output) except PermissionError: print("You do not have a permission to connect to the IPsec daemon") - sys.exit(1) + exit(1) except ConnectionRefusedError: print("IPsec is not runing") - sys.exit(1) + exit(1) except Exception as e: print("An error occured: {0}".format(e)) - sys.exit(1) + exit(1) |