diff options
-rw-r--r-- | data/configd-include.json | 3 | ||||
-rw-r--r-- | data/templates/frr/staticd.frr.j2 | 2 | ||||
-rw-r--r-- | data/templates/frr/zebra.vrf.route-map.frr.j2 | 2 | ||||
-rw-r--r-- | debian/control | 4 | ||||
-rw-r--r-- | interface-definitions/vrf.xml.in | 15 | ||||
-rw-r--r-- | op-mode-definitions/vpn-ipsec.xml.in | 41 | ||||
-rw-r--r-- | src/conf_mode/vrf_vni.py | 86 | ||||
-rwxr-xr-x | src/op_mode/dynamic_dns.py | 16 | ||||
-rwxr-xr-x | src/op_mode/ipsec.py | 216 | ||||
-rwxr-xr-x | src/op_mode/show_vpn_ra.py | 56 |
10 files changed, 364 insertions, 77 deletions
diff --git a/data/configd-include.json b/data/configd-include.json index 2f1d39006..456211caa 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -85,5 +85,6 @@ "vpn_l2tp.py", "vpn_pptp.py", "vpn_sstp.py", -"vrf.py" +"vrf.py", +"vrf_vni.py" ] diff --git a/data/templates/frr/staticd.frr.j2 b/data/templates/frr/staticd.frr.j2 index 55c05ceb7..992a0435c 100644 --- a/data/templates/frr/staticd.frr.j2 +++ b/data/templates/frr/staticd.frr.j2 @@ -37,7 +37,7 @@ vrf {{ vrf }} {% endfor %} {% endif %} {% if vrf is vyos_defined %} - exit-vrf +exit-vrf {% endif %} ! {# Policy route tables #} diff --git a/data/templates/frr/zebra.vrf.route-map.frr.j2 b/data/templates/frr/zebra.vrf.route-map.frr.j2 index eb6abd8e7..3c5791c4c 100644 --- a/data/templates/frr/zebra.vrf.route-map.frr.j2 +++ b/data/templates/frr/zebra.vrf.route-map.frr.j2 @@ -18,7 +18,7 @@ vrf {{ vrf }} {% if vrf_config.vni is vyos_defined %} vni {{ vrf_config.vni }} {% endif %} +exit-vrf {% endfor %} - exit-vrf ! {% endif %} diff --git a/debian/control b/debian/control index 3126e6ad9..4a2706fc3 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,7 @@ Build-Depends: build-essential, libvyosconfig0 (>= 0.0.7), libzmq3-dev, - python3, + python3 (>= 3.10), python3-coverage, python3-lxml, python3-netifaces, @@ -33,7 +33,7 @@ Standards-Version: 3.9.6 Package: vyos-1x Architecture: amd64 arm64 Depends: - ${python3:Depends}, + ${python3:Depends} (>= 3.10), aardvark-dns, accel-ppp, auditd, diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index a7efe146a..3783785ce 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -121,7 +121,20 @@ <constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage> </properties> </leafNode> - #include <include/vni.xml.i> + <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py $VAR(../@)"> + <properties> + <help>Virtual Network Identifier</help> + <!-- must be after BGP to keep correct order when removing L3VNIs in FRR --> + <priority>822</priority> + <valueHelp> + <format>u32:0-16777214</format> + <description>VXLAN virtual network identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777214"/> + </constraint> + </properties> + </leafNode> </children> </tagNode> </children> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index 5a7e6dd63..1eb5a3709 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -204,12 +204,37 @@ </properties> <command>sudo ip xfrm policy list</command> </node> - <leafNode name="remote-access"> - <properties> - <help>Show active VPN server sessions</help> - </properties> - <command>${vyos_op_scripts_dir}/show_vpn_ra.py</command> - </leafNode> + <node name="remote-access"> + <properties> + <help>Show active VPN server sessions</help> + </properties> + <children> + <node name="detail"> + <properties> + <help>Show detail active IKEv2 RA sessions</help> + </properties> + <command>if systemctl is-active --quiet strongswan ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_ra_detail; else echo "IPsec process not running" ; fi</command> + </node> + <tagNode name="connection-id"> + <properties> + <help>Show detail active IKEv2 RA sessions by connection-id</help> + </properties> + <command>if systemctl is-active --quiet strongswan ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_ra_detail --conn_id="$6"; else echo "IPsec process not running" ; fi</command> + </tagNode> + <node name="summary"> + <properties> + <help>Show active IKEv2 RA sessions summary</help> + </properties> + <command>if systemctl is-active --quiet strongswan ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_ra_summary; else echo "IPsec process not running" ; fi</command> + </node> + <tagNode name="username"> + <properties> + <help>Show detail active IKEv2 RA sessions by username</help> + </properties> + <command>if systemctl is-active --quiet strongswan ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_ra_detail --username="$6"; else echo "IPsec process not running" ; fi</command> + </tagNode> + </children> + </node> <node name="sa"> <properties> <help>Show all active IPsec Security Associations (SA)</help> @@ -241,11 +266,11 @@ <command></command> </tagNode> --> - <node name="verbose"> + <node name="detail"> <properties> <help>Show Verbose Detail on all active IPsec Security Associations (SA)</help> </properties> - <command>if systemctl is-active --quiet strongswan ; then sudo /usr/sbin/ipsec statusall ; else echo "IPsec process not running" ; fi</command> + <command>if systemctl is-active --quiet strongswan ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_sa_detail ; else echo "IPsec process not running" ; fi</command> </node> </children> <command>if systemctl is-active --quiet strongswan ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_sa ; else echo "IPsec process not running" ; fi</command> diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py new file mode 100644 index 000000000..0cf52a2ef --- /dev/null +++ b/src/conf_mode/vrf_vni.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +from sys import argv +from sys import exit + +from vyos.config import Config +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + vrf_name = None + if len(argv) > 1: + vrf_name = argv[1] + + base = ['vrf', 'name', vrf_name] + tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=False) + if not tmp: + return None + vrf = { 'name' : conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=False) } + + return vrf + +def verify(vrf): + if not vrf: + return + + if len(argv) < 2: + raise ConfigError('VRF parameter not specified when valling vrf_vni.py') + + return None + +def generate(vrf): + if not vrf: + return + + vrf['new_frr_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) + return None + +def apply(vrf): + frr_daemon = 'zebra' + + # add configuration to FRR + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration(frr_daemon) + # There is only one VRF inside the dict as we read only one in get_config() + if vrf and 'name' in vrf: + vrf_name = next(iter(vrf['name'])) + frr_cfg.modify_section(f'^vrf {vrf_name}', stop_pattern='^exit-vrf', remove_stop_mark=True) + if vrf and 'new_frr_config' in vrf: + frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) + frr_cfg.commit_configuration(frr_daemon) + + 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/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 2cba33cc8..d41a74db3 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -21,6 +21,7 @@ import time from tabulate import tabulate from vyos.config import Config +from vyos.template import is_ipv4, is_ipv6 from vyos.util import call cache_file = r'/run/ddclient/ddclient.cache' @@ -46,7 +47,7 @@ def _get_formatted_host_records(host_data): def show_status(): - # A ddclient status file must not always exist + # A ddclient status file might not always exist if not os.path.exists(cache_file): sys.exit(0) @@ -62,9 +63,20 @@ def show_status(): # we pick up the ones we are interested in for kvraw in line.split(' ')[0].split(','): k, v = kvraw.split('=') - if k in columns.keys(): + if k in list(columns.keys()) + ['ip', 'status']: # ip and status are legacy keys props[k] = v + # Extract IPv4 and IPv6 address and status from legacy keys + # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 + if 'ip' in props: + if is_ipv4(props['ip']): + props['ipv4'] = props['ip'] + props['status-ipv4'] = props['status'] + elif is_ipv6(props['ip']): + props['ipv6'] = props['ip'] + props['status-ipv6'] = props['status'] + del props['ip'] + # Convert mtime to human readable format if 'mtime' in props: props['mtime'] = time.strftime( diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 7f4fb72e5..db4948d7a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -13,7 +13,6 @@ # # 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 import typing @@ -24,6 +23,7 @@ from tabulate import tabulate from vyos.util import convert_data from vyos.util import seconds_to_human +from vyos.util import cmd from vyos.configquery import ConfigTreeQuery import vyos.opmode @@ -46,6 +46,25 @@ def _get_raw_data_sas(): except (vyos.ipsec.ViciInitiateError) as err: raise vyos.opmode.UnconfiguredSubsystem(err) + +def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str: + """ + Template for output for VICI + Inserts \n after each IKE SA + :param ra_output_list: IKE SAs list + :type ra_output_list: list + :return: formatted string + :rtype: str + """ + output = ''; + for sa_val in ra_output_list: + for sa in sa_val.values(): + swanctl_output: str = cmd( + f'sudo swanctl -l --ike-id {sa["uniqueid"]}') + output = f'{output}{swanctl_output}\n\n' + return output + + def _get_formatted_output_sas(sas): sa_data = [] for sa in sas: @@ -444,6 +463,7 @@ def reset_peer(peer: str, tunnel: typing.Optional[str] = None): except (vyos.ipsec.ViciCommandError) as err: raise vyos.opmode.IncorrectValue(err) + def reset_all_peers(): sitetosite_list = _get_all_sitetosite_peers_name_list() if sitetosite_list: @@ -457,6 +477,7 @@ def reset_all_peers(): raise vyos.opmode.UnconfiguredSubsystem( 'VPN IPSec site-to-site is not configured, aborting') + def _get_ra_session_list_by_username(username: typing.Optional[str] = None): """ Return list of remote-access IKE_SAs uniqueids @@ -466,15 +487,15 @@ def _get_ra_session_list_by_username(username: typing.Optional[str] = None): :rtype: """ list_sa_id = [] - sa_list = vyos.ipsec.get_vici_sas() + sa_list = _get_raw_data_sas() for sa_val in sa_list: for sa in sa_val.values(): if 'remote-eap-id' in sa: if username: - if username == sa['remote-eap-id'].decode(): - list_sa_id.append(sa['uniqueid'].decode()) + if username == sa['remote-eap-id']: + list_sa_id.append(sa['uniqueid']) else: - list_sa_id.append(sa['uniqueid'].decode()) + list_sa_id.append(sa['uniqueid']) return list_sa_id @@ -556,6 +577,24 @@ def show_sa(raw: bool): return _get_formatted_output_sas(sa_data) +def _get_output_sas_detail(ra_output_list: list) -> str: + """ + Formate all IKE SAs detail output + :param ra_output_list: IKE SAs list + :type ra_output_list: list + :return: formatted RA IKE SAs detail output + :rtype: str + """ + return _get_output_swanctl_sas_from_list(ra_output_list) + + +def show_sa_detail(raw: bool): + sa_data = _get_raw_data_sas() + if raw: + return sa_data + return _get_output_sas_detail(sa_data) + + def show_connections(raw: bool): list_conns = _get_convert_data_connections() list_sas = _get_raw_data_sas() @@ -573,6 +612,173 @@ def show_connections_summary(raw: bool): return _get_raw_connections_summary(list_conns, list_sas) +def _get_ra_sessions(username: typing.Optional[str] = None) -> list: + """ + Return list of remote-access IKE_SAs from VICI by username. + If username unspecified, return all remote-access IKE_SAs + :param username: Username of RA connection + :type username: str + :return: list of ra remote-access IKE_SAs + :rtype: list + """ + list_sa = [] + sa_list = _get_raw_data_sas() + for conn in sa_list: + for sa in conn.values(): + if 'remote-eap-id' in sa: + if username: + if username == sa['remote-eap-id']: + list_sa.append(conn) + else: + list_sa.append(conn) + return list_sa + + +def _filter_ikesas(list_sa: list, filter_key: str, filter_value: str) -> list: + """ + Filter IKE SAs by specifice key + :param list_sa: list of IKE SAs + :type list_sa: list + :param filter_key: Filter Key + :type filter_key: str + :param filter_value: Filter Value + :type filter_value: str + :return: Filtered list of IKE SAs + :rtype: list + """ + filtered_sa_list = [] + for conn in list_sa: + for sa in conn.values(): + if sa[filter_key] and sa[filter_key] == filter_value: + filtered_sa_list.append(conn) + return filtered_sa_list + + +def _get_last_installed_childsa(sa: dict) -> str: + """ + Return name of last installed active Child SA + :param sa: Dictionary with Child SAs + :type sa: dict + :return: Name of the Last installed active Child SA + :rtype: str + """ + child_sa_name = None + child_sa_id = 0 + for sa_name, child_sa in sa['child-sas'].items(): + if child_sa['state'] == 'INSTALLED': + if child_sa_id == 0 or int(child_sa['uniqueid']) > child_sa_id: + child_sa_id = int(child_sa['uniqueid']) + child_sa_name = sa_name + return child_sa_name + + +def _get_formatted_ike_proposal(sa: dict) -> str: + """ + Return IKE proposal string in format + EncrALG-EncrKeySize/PFR/HASH/DH-GROUP + :param sa: IKE SA + :type sa: dict + :return: IKE proposal string + :rtype: str + """ + proposal = '' + proposal = f'{proposal}{sa["encr-alg"]}' if 'encr-alg' in sa else proposal + proposal = f'{proposal}-{sa["encr-keysize"]}' if 'encr-keysize' in sa else proposal + proposal = f'{proposal}/{sa["prf-alg"]}' if 'prf-alg' in sa else proposal + proposal = f'{proposal}/{sa["integ-alg"]}' if 'integ-alg' in sa else proposal + proposal = f'{proposal}/{sa["dh-group"]}' if 'dh-group' in sa else proposal + return proposal + + +def _get_formatted_ipsec_proposal(sa: dict) -> str: + """ + Return IPSec proposal string in format + Protocol: EncrALG-EncrKeySize/HASH/PFS + :param sa: Child SA + :type sa: dict + :return: IPSec proposal string + :rtype: str + """ + proposal = '' + proposal = f'{proposal}{sa["protocol"]}' if 'protocol' in sa else proposal + proposal = f'{proposal}:{sa["encr-alg"]}' if 'encr-alg' in sa else proposal + proposal = f'{proposal}-{sa["encr-keysize"]}' if 'encr-keysize' in sa else proposal + proposal = f'{proposal}/{sa["integ-alg"]}' if 'integ-alg' in sa else proposal + proposal = f'{proposal}/{sa["dh-group"]}' if 'dh-group' in sa else proposal + return proposal + + +def _get_output_ra_sas_detail(ra_output_list: list) -> str: + """ + Formate RA IKE SAs detail output + :param ra_output_list: IKE SAs list + :type ra_output_list: list + :return: formatted RA IKE SAs detail output + :rtype: str + """ + return _get_output_swanctl_sas_from_list(ra_output_list) + + +def _get_formatted_output_ra_summary(ra_output_list: list): + sa_data = [] + for conn in ra_output_list: + for sa in conn.values(): + sa_id = sa['uniqueid'] if 'uniqueid' in sa else '' + sa_username = sa['remote-eap-id'] if 'remote-eap-id' in sa else '' + sa_protocol = f'IKEv{sa["version"]}' if 'version' in sa else '' + sa_remotehost = sa['remote-host'] if 'remote-host' in sa else '' + sa_remoteid = sa['remote-id'] if 'remote-id' in sa else '' + sa_ike_proposal = _get_formatted_ike_proposal(sa) + sa_tunnel_ip = sa['remote-vips'] + child_sa_key = _get_last_installed_childsa(sa) + if child_sa_key: + child_sa = sa['child-sas'][child_sa_key] + sa_ipsec_proposal = _get_formatted_ipsec_proposal(child_sa) + sa_state = "UP" + sa_uptime = seconds_to_human(sa['established']) + else: + sa_ipsec_proposal = '' + sa_state = "DOWN" + sa_uptime = '' + sa_data.append( + [sa_id, sa_username, sa_protocol, sa_state, sa_uptime, + sa_tunnel_ip, + sa_remotehost, sa_remoteid, sa_ike_proposal, + sa_ipsec_proposal]) + + headers = ["Connection ID", "Username", "Protocol", "State", "Uptime", + "Tunnel IP", "Remote Host", "Remote ID", "IKE Proposal", + "IPSec Proposal"] + sa_data = sorted(sa_data, key=_alphanum_key) + output = tabulate(sa_data, headers) + return output + + +def show_ra_detail(raw: bool, username: typing.Optional[str] = None, + conn_id: typing.Optional[str] = None): + list_sa: list = _get_ra_sessions() + if username: + list_sa = _filter_ikesas(list_sa, 'remote-eap-id', username) + elif conn_id: + list_sa = _filter_ikesas(list_sa, 'uniqueid', conn_id) + if not list_sa: + raise vyos.opmode.IncorrectValue( + f'No active connections found, aborting') + if raw: + return list_sa + return _get_output_ra_sas_detail(list_sa) + + +def show_ra_summary(raw: bool): + list_sa: list = _get_ra_sessions() + if not list_sa: + raise vyos.opmode.IncorrectValue( + f'No active connections found, aborting') + if raw: + return list_sa + return _get_formatted_output_ra_summary(list_sa) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py deleted file mode 100755 index 73688c4ea..000000000 --- a/src/op_mode/show_vpn_ra.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 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 os -import sys -import re - -from vyos.util import popen - -# chech connection to pptp and l2tp daemon -def get_sessions(): - absent_pptp = False - absent_l2tp = False - pptp_cmd = "accel-cmd -p 2003 show sessions" - l2tp_cmd = "accel-cmd -p 2004 show sessions" - err_pattern = "^Connection.+failed$" - # This value for chack only output header without sessions. - len_def_header = 170 - - # Check pptp - output, err = popen(pptp_cmd, decode='utf-8') - if not err and len(output) > len_def_header and not re.search(err_pattern, output): - print(output) - else: - absent_pptp = True - - # Check l2tp - output, err = popen(l2tp_cmd, decode='utf-8') - if not err and len(output) > len_def_header and not re.search(err_pattern, output): - print(output) - else: - absent_l2tp = True - - if absent_l2tp and absent_pptp: - print("No active remote access VPN sessions") - - -def main(): - get_sessions() - - -if __name__ == '__main__': - main() |