From 6acf41ea7d11d549cb4453f9aa6f66aaa121aa5e Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Thu, 20 Oct 2022 11:42:38 +0000 Subject: T4763: Use nat.py for show nat destination statistics Use nat.py instead of old op-mode script --- op-mode-definitions/nat.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index ce0544390..50abb1555 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -64,7 +64,7 @@ Show statistics for configured destination NAT rules - ${vyos_op_scripts_dir}/show_nat_statistics.py --destination + ${vyos_op_scripts_dir}/nat.py show_statistics --direction destination --family inet -- cgit v1.2.3 From 551cb56cf752776aa4f9d2336c887ea3127b530e Mon Sep 17 00:00:00 2001 From: goodNETnick Date: Sun, 16 Oct 2022 02:37:26 -0400 Subject: login: T4751: 2FA OTP key generator in VyOS CLI --- .../generate-system-login-user.xml.in | 90 ++++++++++++++++++++++ src/op_mode/generate_system_login_user.py | 77 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100755 op-mode-definitions/generate-system-login-user.xml.in create mode 100755 src/op_mode/generate_system_login_user.py (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/generate-system-login-user.xml.in b/op-mode-definitions/generate-system-login-user.xml.in new file mode 100755 index 000000000..d0519b6bd --- /dev/null +++ b/op-mode-definitions/generate-system-login-user.xml.in @@ -0,0 +1,90 @@ + + + + + + + Generate system related parameters + + + + + Generate system login related parameters + + + + + Username used for authentication + + <username> + + + + + + Generate OpenConnect OTP token + + + + + HOTP time-based token + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" + + + + Duration of single time interval + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "$9" + + + + The number of digits in the one-time password + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "$9" --rate_time "${11}" + + + + The number of digits in the one-time password + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "$9" --rate_time "${11}" --window_size "${13}" + + + + + + + + The number of digits in the one-time password + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --window_size "${9}" + + + + Duration of single time interval + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "${11}" --window_size "${9}" + + + + Duration of single time interval + + sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "${11}" --rate_time "${13}" --window_size "${9}" + + + + + + + + + + + + + + + + + + diff --git a/src/op_mode/generate_system_login_user.py b/src/op_mode/generate_system_login_user.py new file mode 100755 index 000000000..8f8827b1b --- /dev/null +++ b/src/op_mode/generate_system_login_user.py @@ -0,0 +1,77 @@ +#!/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 . + +import argparse +import os + +from vyos.util import popen +from secrets import token_hex +from base64 import b32encode + +if os.geteuid() != 0: + exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.") + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-u", "--username", type=str, help='Username used for authentication', required=True) + parser.add_argument("-l", "--rate_limit", type=str, help='Limit number of logins (rate-limit) per rate-time (default: 3)', default="3", required=False) + parser.add_argument("-t", "--rate_time", type=str, help='Limit number of logins (rate-limit) per rate-time (default: 30)', default="30", required=False) + parser.add_argument("-w", "--window_size", type=str, help='Set window of concurrently valid codes (default: 3)', default="3", required=False) + parser.add_argument("-i", "--interval", type=str, help='Duration of single time interval', default="30", required=False) + parser.add_argument("-d", "--digits", type=str, help='The number of digits in the one-time password', default="6", required=False) + args = parser.parse_args() + + hostname = os.uname()[1] + username = args.username + rate_limit = args.rate_limit + rate_time = args.rate_time + window_size = args.window_size + digits = args.digits + period = args.interval + + # check variables: + if int(rate_limit) < 1 or int(rate_limit) > 10: + print("") + quit("Number of logins (rate-limit) must be between '1' and '10'") + + if int(rate_time) < 15 or int(rate_time) > 600: + print("") + quit("The rate-time must be between '15' and '600' seconds") + + if int(window_size) < 1 or int(window_size) > 21: + print("") + quit("Window of concurrently valid codes must be between '1' and '21' seconds") + + # generate OTP key, URL & QR: + key_hex = token_hex(20) + key_base32 = b32encode(bytes.fromhex(key_hex)).decode() + + otp_url=''.join(["otpauth://totp/",username,"@",hostname,"?secret=",key_base32,"&digits=",digits,"&period=",period]) + qrcode,err = popen('qrencode -t ansiutf8', input=otp_url) + + print("# You can share it with the user, he just needs to scan the QR in his OTP app") + print("# username: ", username) + print("# OTP KEY: ", key_base32) + print("# OTP URL: ", otp_url) + print(qrcode) + print('# To add this OTP key to configuration, run the following commands:') + print(f"set system login user {username} authentication otp key '{key_base32}'") + if rate_limit != "3": + print(f"set system login user {username} authentication otp rate-limit '{rate_limit}'") + if rate_time != "30": + print(f"set system login user {username} authentication otp rate-time '{rate_time}'") + if window_size != "3": + print(f"set system login user {username} authentication otp window-size '{window_size}'") -- cgit v1.2.3 From 738641a6c66d22c09b8c028ee3d8a90527d9701f Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Wed, 19 Oct 2022 14:16:05 +0000 Subject: T4758: Rewrite show DHCP(v6) server leases to vyos.opmode format Rewrite op-mode DHCP and DHCPv6 leases to vyos.opmode format Abbility to show 'raw' format show dhcp server leases show dhcpv6 server leases --- data/op-mode-standardized.json | 2 + op-mode-definitions/dhcp.xml.in | 4 +- src/op_mode/dhcp.py | 278 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+), 2 deletions(-) create mode 100755 src/op_mode/dhcp.py (limited to 'op-mode-definitions') diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 9500d3aa7..03b85b50f 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -3,6 +3,8 @@ "conntrack.py", "container.py", "cpu.py", +"dhcp.py", +"log.py", "memory.py", "nat.py", "neighbor.py", diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index 241cca0ce..ce4026ff4 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -16,7 +16,7 @@ Show DHCP server leases - sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases + sudo ${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet @@ -82,7 +82,7 @@ Show DHCPv6 server leases - sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases + sudo ${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py new file mode 100755 index 000000000..07e9b7d6c --- /dev/null +++ b/src/op_mode/dhcp.py @@ -0,0 +1,278 @@ +#!/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 . + +import sys +from ipaddress import ip_address +import typing + +from datetime import datetime +from sys import exit +from tabulate import tabulate +from isc_dhcp_leases import IscDhcpLeases + +from vyos.base import Warning +from vyos.configquery import ConfigTreeQuery + +from vyos.util import cmd +from vyos.util import dict_search +from vyos.util import is_systemd_service_running + +import vyos.opmode + + +config = ConfigTreeQuery() +pool_key = "shared-networkname" + + +def _in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + return False + + +def _utc_to_local(utc_dt): + return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()) + + +def _format_hex_string(in_str): + out_str = "" + # if input is divisible by 2, add : every 2 chars + if len(in_str) > 0 and len(in_str) % 2 == 0: + out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2])) + else: + out_str = in_str + + return out_str + + +def _find_list_of_dict_index(lst, key='ip', value='') -> int: + """ + Find the index entry of list of dict matching the dict value + Exampe: + % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}] + % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2') + % 1 + """ + idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None) + return idx + + +def _get_raw_server_leases(family, pool=None) -> list: + """ + Get DHCP server leases + :return list + """ + lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' + data = [] + leases = IscDhcpLeases(lease_file).get() + if pool is not None: + if config.exists(f'service dhcp-server shared-network-name {pool}'): + leases = list(filter(lambda x: _in_pool(x, pool), leases)) + for lease in leases: + data_lease = {} + data_lease['ip'] = lease.ip + data_lease['state'] = lease.binding_state + data_lease['pool'] = lease.sets.get('shared-networkname', '') + data_lease['end'] = lease.end.timestamp() + + if family == 'inet': + data_lease['hardware'] = lease.ethernet + data_lease['start'] = lease.start.timestamp() + data_lease['hostname'] = lease.hostname + + if family == 'inet6': + data_lease['last_communication'] = lease.last_communication.timestamp() + data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) + lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} + data_lease['type'] = lease_types_long[lease.type] + + data_lease['remaining'] = lease.end - datetime.utcnow() + + if data_lease['remaining'].days >= 0: + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + else: + data_lease['remaining'] = '' + + # Do not add old leases + if data_lease['remaining'] != '': + data.append(data_lease) + + # deduplicate + checked = [] + for entry in data: + addr = entry.get('ip') + if addr not in checked: + checked.append(addr) + else: + idx = _find_list_of_dict_index(data, key='ip', value=addr) + data.pop(idx) + + return data + + +def _get_formatted_server_leases(raw_data, family): + data_entries = [] + if family == 'inet': + for lease in raw_data: + ipaddr = lease.get('ip') + hw_addr = lease.get('hardware') + state = lease.get('state') + start = lease.get('start') + start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') + end = lease.get('end') + end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') + remain = lease.get('remaining') + pool = lease.get('pool') + hostname = lease.get('hostname') + data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname]) + + headers = ['IP Address', 'Hardware address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool', + 'Hostname'] + + if family == 'inet6': + for lease in raw_data: + ipaddr = lease.get('ip') + state = lease.get('state') + start = lease.get('last_communication') + start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') + end = lease.get('end') + end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') + remain = lease.get('remaining') + lease_type = lease.get('type') + pool = lease.get('pool') + host_identifier = lease.get('iaid_duid') + data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier]) + + headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool', + 'IAID_DUID'] + + output = tabulate(data_entries, headers, numalign='left') + return output + + +def _get_dhcp_pools(family='inet') -> list: + v = 'v6' if family == 'inet6' else '' + pools = config.list_nodes(f'service dhcp{v}-server shared-network-name') + return pools + + +def _get_pool_size(pool, family='inet'): + v = 'v6' if family == 'inet6' else '' + base = f'service dhcp{v}-server shared-network-name {pool}' + size = 0 + subnets = config.list_nodes(f'{base} subnet') + for subnet in subnets: + if family == 'inet6': + ranges = config.list_nodes(f'{base} subnet {subnet} address-range start') + else: + ranges = config.list_nodes(f'{base} subnet {subnet} range') + for range in ranges: + if family == 'inet6': + start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0] + stop = config.value(f'{base} subnet {subnet} address-range start {start} stop') + else: + start = config.value(f'{base} subnet {subnet} range {range} start') + stop = config.value(f'{base} subnet {subnet} range {range} stop') + # Add +1 because both range boundaries are inclusive + size += int(ip_address(stop)) - int(ip_address(start)) + 1 + return size + + +def _get_raw_pool_statistics(family='inet', pool=None): + if pool is None: + pool = _get_dhcp_pools(family=family) + else: + pool = [pool] + + v = 'v6' if family == 'inet6' else '' + stats = [] + for p in pool: + subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet') + size = _get_pool_size(family=family, pool=p) + leases = len(_get_raw_server_leases(family=family, pool=p)) + use_percentage = round(leases / size * 100) if size != 0 else 0 + pool_stats = {'pool': p, 'size': size, 'leases': leases, + 'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet} + stats.append(pool_stats) + return stats + + +def _get_formatted_pool_statistics(pool_data, family='inet'): + data_entries = [] + for entry in pool_data: + pool = entry.get('pool') + size = entry.get('size') + leases = entry.get('leases') + available = entry.get('available') + use_percentage = entry.get('use_percentage') + use_percentage = f'{use_percentage}%' + data_entries.append([pool, size, leases, available, use_percentage]) + + headers = ['Pool', 'Size','Leases', 'Available', 'Usage'] + output = tabulate(data_entries, headers, numalign='left') + return output + + +def _verify(func): + """Decorator checks if DHCP(v6) config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + family = kwargs.get('family') + v = 'v6' if family == 'inet6' else '' + unconf_message = f'DHCP{v} server is not configured' + # Check if config does not exist + if not config.exists(f'service dhcp{v}-server'): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper + + +@_verify +def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]): + pool_data = _get_raw_pool_statistics(family=family, pool=pool) + if raw: + return pool_data + else: + return _get_formatted_pool_statistics(pool_data, family=family) + + +@_verify +def show_server_leases(raw: bool, family: str): + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if not is_systemd_service_running('isc-dhcp-server.service'): + Warning('DHCP server is configured but not started. Data may be stale.') + + leases = _get_raw_server_leases(family) + if raw: + return leases + else: + return _get_formatted_server_leases(leases, family) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 4245fd8fb1059d0a356e0fcb293fcbb923b09b68 Mon Sep 17 00:00:00 2001 From: "m.korobeinikov" Date: Tue, 8 Nov 2022 08:56:42 +1000 Subject: T4767: Rewrite generate ipsec archive to python Made the following changes: 1) made changes to the "XML" file to replace the script from "sh" to "py" 2) changed the extension of the main script from "sh" to "py" 3) changed the script to "py" --- .../generate-ipsec-debug-archive.xml.in | 2 +- src/op_mode/generate_ipsec_debug_archive.py | 90 ++++++++++++++++++++++ src/op_mode/generate_ipsec_debug_archive.sh | 36 --------- 3 files changed, 91 insertions(+), 37 deletions(-) create mode 100755 src/op_mode/generate_ipsec_debug_archive.py delete mode 100755 src/op_mode/generate_ipsec_debug_archive.sh (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/generate-ipsec-debug-archive.xml.in b/op-mode-definitions/generate-ipsec-debug-archive.xml.in index f268d5ae5..dcbed0c42 100644 --- a/op-mode-definitions/generate-ipsec-debug-archive.xml.in +++ b/op-mode-definitions/generate-ipsec-debug-archive.xml.in @@ -8,7 +8,7 @@ Generate IPSec debug-archive - ${vyos_op_scripts_dir}/generate_ipsec_debug_archive.sh + ${vyos_op_scripts_dir}/generate_ipsec_debug_archive.py diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py new file mode 100755 index 000000000..933dd4e1a --- /dev/null +++ b/src/op_mode/generate_ipsec_debug_archive.py @@ -0,0 +1,90 @@ +#!/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 . + +from datetime import datetime +from pathlib import Path +from shutil import rmtree +from socket import gethostname +from sys import exit +from tarfile import open as tar_open +from vyos.util import rc_cmd + +# define a list of commands that needs to be executed +CMD_LIST: list[str] = [ + 'sudo ipsec status', + 'sudo swanctl -L', + 'sudo swanctl -l', + 'sudo swanctl -P', + 'sudo ip x sa show', + 'sudo ip x policy show', + 'sudo ip tunnel show', + 'sudo ip address', + 'sudo ip rule show', + 'sudo ip route | head -100', + 'sudo ip route show table 220' +] +JOURNALCTL_CMD: str = 'sudo journalctl -b -n 10000 /usr/lib/ipsec/charon' + + +# execute a command and save the output to a file +def save_stdout(command: str, file: Path) -> None: + rc, stdout = rc_cmd(command) + body: str = f'''### {command} ### +Command: {command} +Exit code: {rc} +Stdout: +{stdout} + +''' + with file.open(mode='a') as f: + f.write(body) + + +# get local host name +hostname: str = gethostname() +# get current time +time_now: str = datetime.now().isoformat(timespec='seconds') + +# define a temporary directory for logs and collected data +tmp_dir: Path = Path(f'/tmp/ipsec_debug_{time_now}') +# set file paths +ipsec_status_file: Path = Path(f'{tmp_dir}/ipsec_status.txt') +journalctl_charon_file: Path = Path(f'{tmp_dir}/journalctl_charon.txt') +archive_file: str = f'/tmp/ipsec_debug_{time_now}.tar.bz2' + +# create files +tmp_dir.mkdir() +ipsec_status_file.touch() +journalctl_charon_file.touch() + +try: + # execute all commands + for command in CMD_LIST: + save_stdout(command, ipsec_status_file) + save_stdout(JOURNALCTL_CMD, journalctl_charon_file) + + # create an archive + with tar_open(name=archive_file, mode='x:bz2') as tar_file: + tar_file.add(tmp_dir) + + # inform user about success + print(f'Debug file is generated and located in {archive_file}') +except Exception as err: + print(f'Error during generating a debug file: {err}') +finally: + # cleanup + rmtree(tmp_dir) + exit() diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh deleted file mode 100755 index 53d0a6eaa..000000000 --- a/src/op_mode/generate_ipsec_debug_archive.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -# Collecting IPSec Debug Information - -DATE=`date +%d-%m-%Y` - -a_CMD=( - "sudo ipsec status" - "sudo swanctl -L" - "sudo swanctl -l" - "sudo swanctl -P" - "sudo ip x sa show" - "sudo ip x policy show" - "sudo ip tunnel show" - "sudo ip address" - "sudo ip rule show" - "sudo ip route" - "sudo ip route show table 220" - ) - - -echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt -date >> /tmp/ipsec-status-${DATE}.txt - -# Execute all DEBUG commands and save it to file -for cmd in "${a_CMD[@]}"; do - echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt - ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null -done - -# Collect charon logs, build .tgz archive -sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \ -sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null -sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt - -echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz" -- cgit v1.2.3 From f60706b4a9bf428e4dd3a170895c539f08286a78 Mon Sep 17 00:00:00 2001 From: initramfs Date: Thu, 10 Nov 2022 15:38:35 +0800 Subject: op-mode: T4810: fix show/monitor log of pppoe interface --- op-mode-definitions/monitor-log.xml.in | 2 +- op-mode-definitions/show-log.xml.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 01462ad8f..dccdfaf9a 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -118,7 +118,7 @@ - journalctl --no-hostname --boot --follow --unit "ppp@$6.service" + journalctl --no-hostname --boot --follow --unit "ppp@$5.service" diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 8906d9ef3..404de1913 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -267,7 +267,7 @@ - journalctl --no-hostname --boot --unit "ppp@$6.service" + journalctl --no-hostname --boot --unit "ppp@$5.service" -- cgit v1.2.3 From 2c4b6b74317240eedbbae9d2816f86eda464ae68 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Mon, 14 Nov 2022 16:54:24 +0000 Subject: T4812: Add op-mode Show vpn ipsec connections Add op-mode CLI "show vpn ipsec connections" Add the ability to show all configured connections/tunnels and their states. Ability to get --raw data --- op-mode-definitions/vpn-ipsec.xml.in | 6 + src/op_mode/ipsec.py | 283 +++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index f1af65fcb..803ce4cc2 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -137,6 +137,12 @@ Show Internet Protocol Security (IPsec) information + + + Show VPN connections + + sudo ${vyos_op_scripts_dir}/ipsec.py show_connections + Show the in-kernel crypto policies diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index aaa0cec5a..75579a447 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -135,6 +135,272 @@ def _get_formatted_output_sas(sas): return output +# Connections block +def _get_vici_connections(): + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") + connections = list(session.list_conns()) + return connections + + +def _get_convert_data_connections(): + get_connections = _get_vici_connections() + connections = convert_data(get_connections) + return connections + + +def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: + """Get parent SA proposals by connection name + if connections not in the 'down' state + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + str: Parent SA connection proposal + AES_CBC/256/HMAC_SHA2_256_128/MODP_1024 + """ + if not data: + return {} + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return {} + if 'encr-alg' in sa[connection_name]: + encr_alg = sa.get(connection_name, '').get('encr-alg') + cipher = encr_alg.split('_')[0] + mode = encr_alg.split('_')[1] + encr_keysize = sa.get(connection_name, '').get('encr-keysize') + integ_alg = sa.get(connection_name, '').get('integ-alg') + # prf_alg = sa.get(connection_name, '').get('prf-alg') + dh_group = sa.get(connection_name, '').get('dh-group') + proposal = { + 'cipher': cipher, + 'mode': mode, + 'key_size': encr_keysize, + 'hash': integ_alg, + 'dh': dh_group + } + return proposal + return {} + + +def _get_parent_sa_state(connection_name: str, data: list) -> str: + """Get parent SA state by connection name + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + Parent SA connection state + """ + if not data: + return 'down' + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return 'down' + if sa[connection_name]['state'].lower() == 'established': + return 'up' + else: + return 'down' + + +def _get_child_sa_state(connection_name: str, tunnel_name: str, + data: list) -> str: + """Get child SA state by connection and tunnel name + + Args: + connection_name (str): Connection name + tunnel_name (str): Tunnel name + data (list): List of current SAs from vici + + Returns: + str: `up` if child SA state is 'installed' otherwise `down` + """ + if not data: + return 'down' + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return 'down' + child_sas = sa[connection_name]['child-sas'] + # Get all child SA states + # there can be multiple SAs per tunnel + child_sa_states = [ + v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name + ] + return 'up' if 'INSTALLED' in child_sa_states else 'down' + + +def _get_child_sa_info(connection_name: str, tunnel_name: str, + data: list) -> dict: + """Get child SA installed info by connection and tunnel name + + Args: + connection_name (str): Connection name + tunnel_name (str): Tunnel name + data (list): List of current SAs from vici + + Returns: + dict: Info of the child SA in the dictionary format + """ + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return {} + child_sas = sa[connection_name]['child-sas'] + # Get all child SA data + # Skip temp SA name (first key), get only SA values as dict + # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...} + # i.e get all data after 'OFFICE-B-tunnel-0-46' + child_sa_info = [ + v for k, v in child_sas.items() if 'name' in v and + v['name'] == tunnel_name and v['state'] == 'INSTALLED' + ] + return child_sa_info[-1] if child_sa_info else {} + + +def _get_child_sa_proposal(child_sa_data: dict) -> dict: + if child_sa_data and 'encr-alg' in child_sa_data: + encr_alg = child_sa_data.get('encr-alg') + cipher = encr_alg.split('_')[0] + mode = encr_alg.split('_')[1] + key_size = child_sa_data.get('encr-keysize') + integ_alg = child_sa_data.get('integ-alg') + dh_group = child_sa_data.get('dh-group') + proposal = { + 'cipher': cipher, + 'mode': mode, + 'key_size': key_size, + 'hash': integ_alg, + 'dh': dh_group + } + return proposal + return {} + + +def _get_raw_data_connections(list_connections: list, list_sas: list) -> list: + """Get configured VPN IKE connections and IPsec states + + Args: + list_connections (list): List of configured connections from vici + list_sas (list): List of current SAs from vici + + Returns: + list: List and status of IKE/IPsec connections/tunnels + """ + base_dict = [] + for connections in list_connections: + base_list = {} + for connection, conn_conf in connections.items(): + base_list['ike_connection_name'] = connection + base_list['ike_connection_state'] = _get_parent_sa_state( + connection, list_sas) + base_list['ike_remote_address'] = conn_conf['remote_addrs'] + base_list['ike_proposal'] = _get_parent_sa_proposal( + connection, list_sas) + base_list['local_id'] = conn_conf.get('local-1', '').get('id') + base_list['remote_id'] = conn_conf.get('remote-1', '').get('id') + base_list['version'] = conn_conf.get('version', 'IKE') + base_list['children'] = [] + children = conn_conf['children'] + for tunnel, tun_options in children.items(): + state = _get_child_sa_state(connection, tunnel, list_sas) + local_ts = tun_options.get('local-ts') + remote_ts = tun_options.get('remote-ts') + dpd_action = tun_options.get('dpd_action') + close_action = tun_options.get('close_action') + sa_info = _get_child_sa_info(connection, tunnel, list_sas) + esp_proposal = _get_child_sa_proposal(sa_info) + base_list['children'].append({ + 'name': tunnel, + 'state': state, + 'local_ts': local_ts, + 'remote_ts': remote_ts, + 'dpd_action': dpd_action, + 'close_action': close_action, + 'sa': sa_info, + 'esp_proposal': esp_proposal + }) + base_dict.append(base_list) + return base_dict + + +def _get_raw_connections_summary(list_conn, list_sas): + import jmespath + data = _get_raw_data_connections(list_conn, list_sas) + match = '[*].children[]' + child = jmespath.search(match, data) + tunnels_down = len([k for k in child if k['state'] == 'down']) + tunnels_up = len([k for k in child if k['state'] == 'up']) + tun_dict = { + 'tunnels': child, + 'total': len(child), + 'down': tunnels_down, + 'up': tunnels_up + } + return tun_dict + + +def _get_formatted_output_conections(data): + from tabulate import tabulate + data_entries = '' + connections = [] + for entry in data: + tunnels = [] + ike_name = entry['ike_connection_name'] + ike_state = entry['ike_connection_state'] + conn_type = entry.get('version', 'IKE') + remote_addrs = ','.join(entry['ike_remote_address']) + local_ts, remote_ts = '-', '-' + local_id = entry['local_id'] + remote_id = entry['remote_id'] + proposal = '-' + if entry.get('ike_proposal'): + proposal = (f'{entry["ike_proposal"]["cipher"]}_' + f'{entry["ike_proposal"]["mode"]}/' + f'{entry["ike_proposal"]["key_size"]}/' + f'{entry["ike_proposal"]["hash"]}/' + f'{entry["ike_proposal"]["dh"]}') + connections.append([ + ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts, + local_id, remote_id, proposal + ]) + for tun in entry['children']: + tun_name = tun.get('name') + tun_state = tun.get('state') + conn_type = 'IPsec' + local_ts = '\n'.join(tun.get('local_ts')) + remote_ts = '\n'.join(tun.get('remote_ts')) + proposal = '-' + if tun.get('esp_proposal'): + proposal = (f'{tun["esp_proposal"]["cipher"]}_' + f'{tun["esp_proposal"]["mode"]}/' + f'{tun["esp_proposal"]["key_size"]}/' + f'{tun["esp_proposal"]["hash"]}/' + f'{tun["esp_proposal"]["dh"]}') + connections.append([ + tun_name, tun_state, conn_type, remote_addrs, local_ts, + remote_ts, local_id, remote_id, proposal + ]) + connection_headers = [ + 'Connection', 'State', 'Type', 'Remote address', 'Local TS', + 'Remote TS', 'Local id', 'Remote id', 'Proposal' + ] + output = tabulate(connections, connection_headers, numalign='left') + return output + + +# Connections block end + + def get_peer_connections(peer, tunnel, return_all = False): search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*' matches = [] @@ -179,6 +445,23 @@ def show_sa(raw: bool): return _get_formatted_output_sas(sa_data) +def show_connections(raw: bool): + list_conns = _get_convert_data_connections() + list_sas = _get_raw_data_sas() + if raw: + return _get_raw_data_connections(list_conns, list_sas) + + connections = _get_raw_data_connections(list_conns, list_sas) + return _get_formatted_output_conections(connections) + + +def show_connections_summary(raw: bool): + list_conns = _get_convert_data_connections() + list_sas = _get_raw_data_sas() + if raw: + return _get_raw_connections_summary(list_conns, list_sas) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) -- cgit v1.2.3 From a1d5124fa73a7ec6850326e8d306fa4a05f2bdf1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 25 Nov 2022 20:49:16 +0100 Subject: veth: T4825: add op-mode CLI definitions --- .../show-interfaces-virtual-ethernet.xml.in | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 op-mode-definitions/show-interfaces-virtual-ethernet.xml.in (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in new file mode 100644 index 000000000..c70f1e3d1 --- /dev/null +++ b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in @@ -0,0 +1,42 @@ + + + + + + + + + Show specified virtual-ethernet interface information + + interfaces virtual-ethernet + + + ${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" + + + + Show summary of the specified virtual-ethernet interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief + + + + + + Show virtual-ethernet interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf-type=virtual-ethernet --action=show-brief + + + + Show detailed virtual-ethernet interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf-type=virtual-ethernet --action=show + + + + + + + + -- cgit v1.2.3 From c2a5957a942c8f6070b8f6ff12764e90c1a4c237 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Dec 2022 09:44:25 +0100 Subject: op-mode: T4767: drop sudo calls It's easier and more obvious if the script is called with sudo itself and not spawning a sudo sessionf or each individual command. --- .../generate-ipsec-debug-archive.xml.in | 2 +- src/op_mode/generate_ipsec_debug_archive.py | 25 +++++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/generate-ipsec-debug-archive.xml.in b/op-mode-definitions/generate-ipsec-debug-archive.xml.in index dcbed0c42..a9ce113d1 100644 --- a/op-mode-definitions/generate-ipsec-debug-archive.xml.in +++ b/op-mode-definitions/generate-ipsec-debug-archive.xml.in @@ -8,7 +8,7 @@ Generate IPSec debug-archive - ${vyos_op_scripts_dir}/generate_ipsec_debug_archive.py + sudo ${vyos_op_scripts_dir}/generate_ipsec_debug_archive.py diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py index 933dd4e1a..1422559a8 100755 --- a/src/op_mode/generate_ipsec_debug_archive.py +++ b/src/op_mode/generate_ipsec_debug_archive.py @@ -24,20 +24,19 @@ from vyos.util import rc_cmd # define a list of commands that needs to be executed CMD_LIST: list[str] = [ - 'sudo ipsec status', - 'sudo swanctl -L', - 'sudo swanctl -l', - 'sudo swanctl -P', - 'sudo ip x sa show', - 'sudo ip x policy show', - 'sudo ip tunnel show', - 'sudo ip address', - 'sudo ip rule show', - 'sudo ip route | head -100', - 'sudo ip route show table 220' + 'ipsec status', + 'swanctl -L', + 'swanctl -l', + 'swanctl -P', + 'ip x sa show', + 'ip x policy show', + 'ip tunnel show', + 'ip address', + 'ip rule show', + 'ip route | head -100', + 'ip route show table 220' ] -JOURNALCTL_CMD: str = 'sudo journalctl -b -n 10000 /usr/lib/ipsec/charon' - +JOURNALCTL_CMD: str = 'journalctl -b -n 10000 /usr/lib/ipsec/charon' # execute a command and save the output to a file def save_stdout(command: str, file: Path) -> None: -- cgit v1.2.3 From 7c414a7c9971c16580a723c6bce6c3335b7b7c12 Mon Sep 17 00:00:00 2001 From: Viacheslav Hletenko Date: Fri, 9 Dec 2022 15:29:45 +0000 Subject: T4865: Fix to generate container image from the file In case if we want generate own container image from a Dockerfile and if it requires update or install packages in container we get error. As it tries to use default network 'podman' and do own NAT translations via 'iptables'. If fact we don't use iptables in 1.4 As result it cannot build such image. Use '--net host' to fix it. --- op-mode-definitions/container.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index 97a087ce2..786bd66d3 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -69,7 +69,7 @@ <filename> - sudo podman build --layers --force-rm --tag "$4" $6 + sudo podman build --net host --layers --force-rm --tag "$4" $6 -- cgit v1.2.3 From ff56aeefddaad2d37d3ea32626e1adf3960eaf26 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 11 Dec 2022 19:38:28 +0100 Subject: sstp: T4384: initial implementation of SSTP client CLI vyos@vyos# show interfaces sstpc sstpc sstpc10 { authentication { password vyos user vyos } server sstp.vyos.net ssl { ca-certificate VyOS-CA } } --- data/configd-include.json | 1 + data/templates/sstp-client/peer.j2 | 46 ++++++++ interface-definitions/interfaces-sstpc.xml.in | 47 ++++++++ op-mode-definitions/connect.xml.in | 1 + op-mode-definitions/disconnect.xml.in | 1 + op-mode-definitions/monitor-log.xml.in | 17 +++ op-mode-definitions/show-interfaces-sstpc.xml.in | 51 ++++++++ op-mode-definitions/show-log.xml.in | 17 +++ python/vyos/ifconfig/__init__.py | 3 +- python/vyos/ifconfig/sstpc.py | 40 +++++++ src/conf_mode/interfaces-sstpc.py | 142 +++++++++++++++++++++++ src/etc/ppp/ip-up.d/96-vyos-sstpc-callback | 49 ++++++++ src/op_mode/connect_disconnect.py | 4 +- 13 files changed, 416 insertions(+), 3 deletions(-) create mode 100644 data/templates/sstp-client/peer.j2 create mode 100644 interface-definitions/interfaces-sstpc.xml.in create mode 100644 op-mode-definitions/show-interfaces-sstpc.xml.in create mode 100644 python/vyos/ifconfig/sstpc.py create mode 100755 src/conf_mode/interfaces-sstpc.py create mode 100755 src/etc/ppp/ip-up.d/96-vyos-sstpc-callback (limited to 'op-mode-definitions') diff --git a/data/configd-include.json b/data/configd-include.json index 5a4912e30..648655a8b 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -28,6 +28,7 @@ "interfaces-openvpn.py", "interfaces-pppoe.py", "interfaces-pseudo-ethernet.py", +"interfaces-sstpc.py", "interfaces-tunnel.py", "interfaces-vti.py", "interfaces-vxlan.py", diff --git a/data/templates/sstp-client/peer.j2 b/data/templates/sstp-client/peer.j2 new file mode 100644 index 000000000..1127d0564 --- /dev/null +++ b/data/templates/sstp-client/peer.j2 @@ -0,0 +1,46 @@ +### Autogenerated by interfaces-sstpc.py ### +{{ '# ' ~ description if description is vyos_defined else '' }} + +# Require peer to provide the local IP address if it is not +# specified explicitly in the config file. +noipdefault + +# Don't show the password in logfiles: +hide-password + +remotename {{ ifname }} +linkname {{ ifname }} +ipparam {{ ifname }} +ifname {{ ifname }} +pty "sstpc --ipparam {{ ifname }} --nolaunchpppd {{ server }}:{{ port }} --ca-cert {{ ca_file_path }}" + +# Override any connect script that may have been set in /etc/ppp/options. +connect /bin/true + +# Don't try to authenticate the remote node +noauth + +# We won't want EAP +refuse-eap + +# Don't try to proxy ARP for the remote endpoint. User can set proxy +# arp entries up manually if they wish. More importantly, having +# the "proxyarp" parameter set disables the "defaultroute" option. +noproxyarp + +# Unlimited connection attempts +maxfail 0 + +plugin sstp-pppd-plugin.so +sstp-sock /var/run/sstpc/sstpc-{{ ifname }} + +persist +debug + +{% if authentication is vyos_defined %} +{{ 'user "' + authentication.user + '"' if authentication.user is vyos_defined }} +{{ 'password "' + authentication.password + '"' if authentication.password is vyos_defined }} +{% endif %} + +{{ "usepeerdns" if no_peer_dns is not vyos_defined }} + diff --git a/interface-definitions/interfaces-sstpc.xml.in b/interface-definitions/interfaces-sstpc.xml.in new file mode 100644 index 000000000..30b55a9fa --- /dev/null +++ b/interface-definitions/interfaces-sstpc.xml.in @@ -0,0 +1,47 @@ + + + + + + + Secure Socket Tunneling Protocol (SSTP) client Interface + 460 + + sstpc[0-9]+ + + Secure Socket Tunneling Protocol interface must be named sstpcN + + sstpcN + Secure Socket Tunneling Protocol interface name + + + + #include + #include + #include + #include + #include + #include + #include + + 1452 + + #include + #include + + 443 + + + + Secure Sockets Layer (SSL) configuration + + + #include + + + #include + + + + + diff --git a/op-mode-definitions/connect.xml.in b/op-mode-definitions/connect.xml.in index d0c93195c..116cd6231 100644 --- a/op-mode-definitions/connect.xml.in +++ b/op-mode-definitions/connect.xml.in @@ -20,6 +20,7 @@ Bring up a connection-oriented network interface interfaces pppoe + interfaces sstpc interfaces wwan diff --git a/op-mode-definitions/disconnect.xml.in b/op-mode-definitions/disconnect.xml.in index 4415c0ed2..843998c4f 100644 --- a/op-mode-definitions/disconnect.xml.in +++ b/op-mode-definitions/disconnect.xml.in @@ -10,6 +10,7 @@ Take down a connection-oriented network interface interfaces pppoe + interfaces sstpc interfaces wwan diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index dccdfaf9a..1b1f53dc2 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -224,6 +224,23 @@ journalctl --no-hostname --boot --follow --unit ssh.service + + + Monitor last lines of SSTP client log + + journalctl --no-hostname --boot --follow --unit "ppp@sstpc*.service" + + + + Monitor last lines of SSTP client log for specific interface + + + + + journalctl --no-hostname --boot --follow --unit "ppp@$5.service" + + + Show log for Virtual Private Network (VPN) diff --git a/op-mode-definitions/show-interfaces-sstpc.xml.in b/op-mode-definitions/show-interfaces-sstpc.xml.in new file mode 100644 index 000000000..e66d3a0ac --- /dev/null +++ b/op-mode-definitions/show-interfaces-sstpc.xml.in @@ -0,0 +1,51 @@ + + + + + + + + + Show specified SSTP client interface information + + interfaces sstpc + + + ${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" + + + + Show specified SSTP client interface log + + journalctl --no-hostname --boot --follow --unit "ppp@$4".service + + + + Show specified SSTP client interface statistics + + interfaces sstpc + + + if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi + + + + + + Show SSTP client interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf-type=sstpc --action=show-brief + + + + Show detailed SSTP client interface information + + ${vyos_op_scripts_dir}/show_interfaces.py --intf-type=sstpc --action=show + + + + + + + + diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 404de1913..64a54015b 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -356,6 +356,23 @@ journalctl --no-hostname --boot --unit ssh.service + + + Show log for SSTP client + + journalctl --no-hostname --boot --unit "ppp@sstpc*.service" + + + + Show SSTP client log on specific interface + + + + + journalctl --no-hostname --boot --unit "ppp@$5.service" + + + Show last n changes to messages diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index d1ddaa13e..206b2bba1 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors +# Copyright 2019-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -38,3 +38,4 @@ from vyos.ifconfig.l2tpv3 import L2TPv3If from vyos.ifconfig.macsec import MACsecIf from vyos.ifconfig.veth import VethIf from vyos.ifconfig.wwan import WWANIf +from vyos.ifconfig.sstpc import SSTPCIf diff --git a/python/vyos/ifconfig/sstpc.py b/python/vyos/ifconfig/sstpc.py new file mode 100644 index 000000000..50fc6ee6b --- /dev/null +++ b/python/vyos/ifconfig/sstpc.py @@ -0,0 +1,40 @@ +# Copyright 2022 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +from vyos.ifconfig.interface import Interface + +@Interface.register +class SSTPCIf(Interface): + iftype = 'sstpc' + definition = { + **Interface.definition, + **{ + 'section': 'sstpc', + 'prefixes': ['sstpc', ], + 'eternal': 'sstpc[0-9]+$', + }, + } + + def _create(self): + # we can not create this interface as it is managed outside + pass + + def _delete(self): + # we can not create this interface as it is managed outside + pass + + def get_mac(self): + """ Get a synthetic MAC address. """ + return self.get_mac_synthetic() diff --git a/src/conf_mode/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py new file mode 100755 index 000000000..6b8094c51 --- /dev/null +++ b/src/conf_mode/interfaces-sstpc.py @@ -0,0 +1,142 @@ +#!/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 . + +import os +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed +from vyos.configverify import verify_authentication +from vyos.configverify import verify_vrf +from vyos.ifconfig import SSTPCIf +from vyos.pki import encode_certificate +from vyos.pki import find_chain +from vyos.pki import load_certificate +from vyos.template import render +from vyos.util import call +from vyos.util import dict_search +from vyos.util import is_systemd_service_running +from vyos.util import write_file +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'sstpc'] + ifname, sstpc = get_interface_dict(conf, base) + + # We should only terminate the SSTP client session if critical parameters + # change. All parameters that can be changed on-the-fly (like interface + # description) should not lead to a reconnect! + for options in ['authentication', 'no_peer_dns', 'no_default_route', + 'server', 'ssl']: + if is_node_changed(conf, base + [ifname, options]): + sstpc.update({'shutdown_required': {}}) + # bail out early - no need to further process other nodes + break + + # Load PKI certificates for later processing + sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + return sstpc + +def verify(sstpc): + if 'deleted' in sstpc: + return None + + verify_authentication(sstpc) + verify_vrf(sstpc) + + if dict_search('ssl.ca_certificate', sstpc) == None: + raise ConfigError('Missing mandatory CA certificate!') + + return None + +def generate(sstpc): + ifname = sstpc['ifname'] + config_sstpc = f'/etc/ppp/peers/{ifname}' + + sstpc['ca_file_path'] = f'/run/sstpc/{ifname}_ca-cert.pem' + + if 'deleted' in sstpc: + for file in [sstpc['ca_file_path'], config_sstpc]: + if os.path.exists(file): + os.unlink(file) + return None + + ca_name = sstpc['ssl']['ca_certificate'] + pki_ca_cert = sstpc['pki']['ca'][ca_name] + + loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) + loaded_ca_certs = {load_certificate(c['certificate']) + for c in sstpc['pki']['ca'].values()} if 'ca' in sstpc['pki'] else {} + + ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) + + write_file(sstpc['ca_file_path'], '\n'.join(encode_certificate(c) for c in ca_full_chain)) + render(config_sstpc, 'sstp-client/peer.j2', sstpc, permission=0o640) + + return None + +def apply(sstpc): + ifname = sstpc['ifname'] + if 'deleted' in sstpc or 'disable' in sstpc: + if os.path.isdir(f'/sys/class/net/{ifname}'): + p = SSTPCIf(ifname) + p.remove() + call(f'systemctl stop ppp@{ifname}.service') + return None + + # reconnect should only be necessary when specific options change, + # like server, authentication ... (see get_config() for details) + if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or + 'shutdown_required' in sstpc): + + # cleanup system (e.g. FRR routes first) + if os.path.isdir(f'/sys/class/net/{ifname}'): + p = SSTPCIf(ifname) + p.remove() + + call(f'systemctl restart ppp@{ifname}.service') + # When interface comes "live" a hook is called: + # /etc/ppp/ip-up.d/96-vyos-sstpc-callback + # which triggers SSTPCIf.update() + else: + if os.path.isdir(f'/sys/class/net/{ifname}'): + p = SSTPCIf(ifname) + p.update(sstpc) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback b/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback new file mode 100755 index 000000000..4e8804f29 --- /dev/null +++ b/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback @@ -0,0 +1,49 @@ +#!/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 . + +# This is a Python hook script which is invoked whenever a SSTP client session +# goes "ip-up". It will call into our vyos.ifconfig library and will then +# execute common tasks for the SSTP interface. The reason we have to "hook" this +# is that we can not create a sstpcX interface in advance in linux and then +# connect pppd to this already existing interface. + +from sys import argv +from sys import exit + +from vyos.configquery import ConfigTreeQuery +from vyos.configdict import get_interface_dict +from vyos.ifconfig import SSTPCIf + +# When the ppp link comes up, this script is called with the following +# parameters +# $1 the interface name used by pppd (e.g. ppp3) +# $2 the tty device name +# $3 the tty device speed +# $4 the local IP address for the interface +# $5 the remote IP address +# $6 the parameter specified by the 'ipparam' option to pppd + +if (len(argv) < 7): + exit(1) + +interface = argv[6] + +conf = ConfigTreeQuery() +_, sstpc = get_interface_dict(conf.config, ['interfaces', 'sstpc'], interface) + +# Update the config +p = SSTPCIf(interface) +p.update(sstpc) diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index 936c20bcb..d39e88bf3 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -41,7 +41,7 @@ def check_ppp_running(interface): def connect(interface): """ Connect dialer interface """ - if interface.startswith('ppp'): + if interface.startswith('pppoe') or interface.startswith('sstpc'): check_ppp_interface(interface) # Check if interface is already dialed if os.path.isdir(f'/sys/class/net/{interface}'): @@ -62,7 +62,7 @@ def connect(interface): def disconnect(interface): """ Disconnect dialer interface """ - if interface.startswith('ppp'): + if interface.startswith('pppoe') or interface.startswith('sstpc'): check_ppp_interface(interface) # Check if interface is already down -- cgit v1.2.3 From 1c763b600c61d09df6f71baec9b6109b687289a0 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 7 Dec 2022 19:17:54 -0600 Subject: openvpn: T4770: update op-mode definition openvpn.xml.in for show/reset --- op-mode-definitions/openvpn.xml.in | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'op-mode-definitions') diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index 301688271..aec09fa48 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -23,7 +23,7 @@ - sudo ${vyos_op_scripts_dir}/reset_openvpn.py $4 + sudo ${vyos_op_scripts_dir}/openvpn.py reset --interface $4 @@ -109,19 +109,19 @@ Show tunnel status for OpenVPN client interfaces - sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client + sudo ${vyos_op_scripts_dir}/openvpn.py show --mode client Show tunnel status for OpenVPN server interfaces - sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server + sudo ${vyos_op_scripts_dir}/openvpn.py show --mode server Show tunnel status for OpenVPN site-to-site interfaces - sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site + sudo ${vyos_op_scripts_dir}/openvpn.py show --mode site-to-site -- cgit v1.2.3