summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/accelppp.py133
-rwxr-xr-xsrc/op_mode/bgp.py120
-rwxr-xr-xsrc/op_mode/bridge.py6
-rwxr-xr-xsrc/op_mode/connect_disconnect.py4
-rwxr-xr-xsrc/op_mode/dhcp.py278
-rwxr-xr-xsrc/op_mode/dns.py4
-rwxr-xr-xsrc/op_mode/firewall.py2
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.py89
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.sh36
-rwxr-xr-xsrc/op_mode/ipsec.py321
-rwxr-xr-xsrc/op_mode/log.py94
-rwxr-xr-xsrc/op_mode/memory.py27
-rwxr-xr-xsrc/op_mode/nat.py22
-rwxr-xr-xsrc/op_mode/openconnect.py14
-rwxr-xr-xsrc/op_mode/openvpn.py220
-rwxr-xr-xsrc/op_mode/ping.py83
-rwxr-xr-xsrc/op_mode/policy_route.py42
-rwxr-xr-xsrc/op_mode/route.py19
-rwxr-xr-xsrc/op_mode/show_openvpn.py6
-rwxr-xr-xsrc/op_mode/storage.py18
-rwxr-xr-xsrc/op_mode/traceroute.py85
-rwxr-xr-xsrc/op_mode/vrf.py6
-rwxr-xr-xsrc/op_mode/webproxy_update_blacklist.sh29
23 files changed, 1489 insertions, 169 deletions
diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py
new file mode 100755
index 000000000..2fd045dc3
--- /dev/null
+++ b/src/op_mode/accelppp.py
@@ -0,0 +1,133 @@
+#!/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 sys
+
+import vyos.accel_ppp
+import vyos.opmode
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import rc_cmd
+
+
+accel_dict = {
+ 'ipoe': {
+ 'port': 2002,
+ 'path': 'service ipoe-server'
+ },
+ 'pppoe': {
+ 'port': 2001,
+ 'path': 'service pppoe-server'
+ },
+ 'pptp': {
+ 'port': 2003,
+ 'path': 'vpn pptp'
+ },
+ 'l2tp': {
+ 'port': 2004,
+ 'path': 'vpn l2tp'
+ },
+ 'sstp': {
+ 'port': 2005,
+ 'path': 'vpn sstp'
+ }
+}
+
+
+def _get_raw_statistics(accel_output, pattern):
+ return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':')
+
+
+def _get_raw_sessions(port):
+ cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \
+ 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \
+ 'tx-bytes-raw,rx-pkts,tx-pkts'
+ output = vyos.accel_ppp.accel_cmd(port, cmd_options)
+ parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse(
+ output.splitlines())
+ return parsed_data
+
+
+def _verify(func):
+ """Decorator checks if accel-ppp protocol
+ ipoe/pppoe/pptp/l2tp/sstp is configured
+
+ for example:
+ service ipoe-server
+ vpn sstp
+ """
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ protocol_list = accel_dict.keys()
+ protocol = kwargs.get('protocol')
+ # unknown or incorrect protocol query
+ if protocol not in protocol_list:
+ unconf_message = f'unknown protocol "{protocol}"'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ # Check if config does not exist
+ config_protocol_path = accel_dict[protocol]['path']
+ if not config.exists(config_protocol_path):
+ unconf_message = f'"{config_protocol_path}" is not configured'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def show_statistics(raw: bool, protocol: str):
+ """show accel-cmd statistics
+ CPU utilization and amount of sessions
+
+ protocol: ipoe/pppoe/ppptp/l2tp/sstp
+ """
+ pattern = f'{protocol}:'
+ port = accel_dict[protocol]['port']
+ rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat')
+
+ if raw:
+ return _get_raw_statistics(output, pattern)
+
+ return output
+
+
+@_verify
+def show_sessions(raw: bool, protocol: str):
+ """show accel-cmd sessions
+
+ protocol: ipoe/pppoe/ppptp/l2tp/sstp
+ """
+ port = accel_dict[protocol]['port']
+ if raw:
+ return _get_raw_sessions(port)
+
+ return vyos.accel_ppp.accel_cmd(port,
+ 'show sessions ifname,username,ip,ip6,ip6-dp,'
+ 'calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes')
+
+
+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)
diff --git a/src/op_mode/bgp.py b/src/op_mode/bgp.py
new file mode 100755
index 000000000..23001a9d7
--- /dev/null
+++ b/src/op_mode/bgp.py
@@ -0,0 +1,120 @@
+#!/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/>.
+#
+# Purpose:
+# Displays bgp neighbors information.
+# Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands.
+
+import re
+import sys
+import typing
+
+import jmespath
+from jinja2 import Template
+from humps import decamelize
+
+from vyos.configquery import ConfigTreeQuery
+
+import vyos.opmode
+
+
+frr_command_template = Template("""
+{% if family %}
+ show bgp
+ {{ 'vrf ' ~ vrf if vrf else '' }}
+ {{ 'ipv6' if family == 'inet6' else 'ipv4'}}
+ {{ 'neighbor ' ~ peer if peer else 'summary' }}
+{% endif %}
+
+{% if raw %}
+ json
+{% endif %}
+""")
+
+
+def _verify(func):
+ """Decorator checks if BGP config exists
+ BGP configuration can be present under vrf <tag>
+ If we do npt get arg 'peer' then it can be 'bgp summary'
+ """
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4'
+ global_vrfs = ['all', 'default']
+ peer = kwargs.get('peer')
+ vrf = kwargs.get('vrf')
+ unconf_message = f'BGP or neighbor is not configured'
+ # Add option to check the specific neighbor if we have arg 'peer'
+ peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else ''
+ vrf_opt = ''
+ if vrf and vrf not in global_vrfs:
+ vrf_opt = f'vrf name {vrf}'
+ # Check if config does not exist
+ if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def show_neighbors(raw: bool,
+ family: str,
+ peer: typing.Optional[str],
+ vrf: typing.Optional[str]):
+ kwargs = dict(locals())
+ frr_command = frr_command_template.render(kwargs)
+ frr_command = re.sub(r'\s+', ' ', frr_command)
+
+ from vyos.util import cmd
+ output = cmd(f"vtysh -c '{frr_command}'")
+
+ if raw:
+ from json import loads
+ data = loads(output)
+ # Get list of the peers
+ peers = jmespath.search('*.peers | [0]', data)
+ if peers:
+ # Create new dict, delete old key 'peers'
+ # add key 'peers' neighbors to the list
+ list_peers = []
+ new_dict = jmespath.search('* | [0]', data)
+ if 'peers' in new_dict:
+ new_dict.pop('peers')
+
+ for neighbor, neighbor_options in peers.items():
+ neighbor_options['neighbor'] = neighbor
+ list_peers.append(neighbor_options)
+ new_dict['peers'] = list_peers
+ return decamelize(new_dict)
+ data = jmespath.search('* | [0]', data)
+ return decamelize(data)
+
+ else:
+ return output
+
+
+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)
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
index 5a821a287..d6098c158 100755
--- a/src/op_mode/bridge.py
+++ b/src/op_mode/bridge.py
@@ -32,7 +32,7 @@ def _get_json_data():
"""
Get bridge data format JSON
"""
- return cmd(f'sudo bridge --json link show')
+ return cmd(f'bridge --json link show')
def _get_raw_data_summary():
@@ -48,7 +48,7 @@ def _get_raw_data_vlan():
"""
:returns dict
"""
- json_data = cmd('sudo bridge --json --compressvlans vlan show')
+ json_data = cmd('bridge --json --compressvlans vlan show')
data_dict = json.loads(json_data)
return data_dict
@@ -57,7 +57,7 @@ def _get_raw_data_fdb(bridge):
"""Get MAC-address for the bridge brX
:returns list
"""
- code, json_data = rc_cmd(f'sudo bridge --json fdb show br {bridge}')
+ code, json_data = rc_cmd(f'bridge --json fdb show br {bridge}')
# From iproute2 fdb.c, fdb_show() will only exit(-1) in case of
# non-existent bridge device; raise error.
if code == 255:
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
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 <http://www.gnu.org/licenses/>.
+
+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)
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
index 9e5b1040c..a0e47d7ad 100755
--- a/src/op_mode/dns.py
+++ b/src/op_mode/dns.py
@@ -54,10 +54,10 @@ def _data_to_dict(data, sep="\t") -> dict:
def _get_raw_forwarding_statistics() -> dict:
- command = cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get-all')
+ command = cmd('rec_control --socket-dir=/run/powerdns get-all')
data = _data_to_dict(command)
data['cache-size'] = "{0:.2f}".format( int(
- cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 )
+ cmd('rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 )
return data
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 950feb625..46bda5f7e 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -63,7 +63,7 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
get_first_key=True, no_tag_node_value_mangle=True)
if firewall and interfaces:
if name:
- firewall['interface'] = []
+ firewall['interface'] = {}
else:
if 'name' in firewall:
for fw_name, name_conf in firewall['name'].items():
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..1422559a8
--- /dev/null
+++ b/src/op_mode/generate_ipsec_debug_archive.py
@@ -0,0 +1,89 @@
+#!/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/>.
+
+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] = [
+ '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 = '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"
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 7ec35d7bd..e0d204a0a 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -14,13 +14,16 @@
# 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 re
import sys
+import typing
from collections import OrderedDict
from hurry import filesize
from re import split as re_split
from tabulate import tabulate
+from subprocess import TimeoutExpired
from vyos.util import call
from vyos.util import convert_data
@@ -43,7 +46,10 @@ def _alphanum_key(key):
def _get_vici_sas():
from vici import Session as vici_session
- session = vici_session()
+ try:
+ session = vici_session()
+ except Exception:
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
sas = list(session.list_sas())
return sas
@@ -132,41 +138,305 @@ def _get_formatted_output_sas(sas):
return output
-def get_peer_connections(peer, tunnel, return_all = False):
+# 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):
search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*'
matches = []
+ if not os.path.exists(SWANCTL_CONF):
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
+ suffix = None if tunnel is None else (f'tunnel-{tunnel}' if
+ tunnel.isnumeric() else tunnel)
with open(SWANCTL_CONF, 'r') as f:
for line in f.readlines():
result = re.match(search, line)
if result:
- suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel
- if return_all or (result[2] == suffix):
+ if tunnel is None:
matches.append(result[1])
+ else:
+ if result[2] == suffix:
+ matches.append(result[1])
return matches
-def reset_peer(peer: str, tunnel:str):
- if not peer:
- print('Invalid peer, aborting')
- return
-
- conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all'))
+def reset_peer(peer: str, tunnel:typing.Optional[str]):
+ conns = get_peer_connections(peer, tunnel)
if not conns:
- print('Tunnel(s) not found, aborting')
- return
+ raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting')
- result = True
for conn in conns:
try:
call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10)
call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10)
except TimeoutExpired as e:
- print(f'Timed out while resetting {conn}')
- result = False
-
+ raise vyos.opmode.InternalError(f'Timed out while resetting {conn}')
- print('Peer reset result: ' + ('success' if result else 'failed'))
+ print('Peer reset result: success')
def show_sa(raw: bool):
@@ -176,6 +446,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__])
diff --git a/src/op_mode/log.py b/src/op_mode/log.py
new file mode 100755
index 000000000..b0abd6191
--- /dev/null
+++ b/src/op_mode/log.py
@@ -0,0 +1,94 @@
+#!/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 json
+import re
+import sys
+import typing
+
+from jinja2 import Template
+
+from vyos.util import rc_cmd
+
+import vyos.opmode
+
+journalctl_command_template = Template("""
+--no-hostname
+--quiet
+
+{% if boot %}
+ --boot
+{% endif %}
+
+{% if count %}
+ --lines={{ count }}
+{% endif %}
+
+{% if reverse %}
+ --reverse
+{% endif %}
+
+{% if since %}
+ --since={{ since }}
+{% endif %}
+
+{% if unit %}
+ --unit={{ unit }}
+{% endif %}
+
+{% if utc %}
+ --utc
+{% endif %}
+
+{% if raw %}
+{# By default show 100 only lines for raw option if count does not set #}
+{# Protection from parsing the full log by default #}
+{% if not boot %}
+ --lines={{ '' ~ count if count else '100' }}
+{% endif %}
+ --no-pager
+ --output=json
+{% endif %}
+""")
+
+
+def show(raw: bool,
+ boot: typing.Optional[bool],
+ count: typing.Optional[int],
+ facility: typing.Optional[str],
+ reverse: typing.Optional[bool],
+ utc: typing.Optional[bool],
+ unit: typing.Optional[str]):
+ kwargs = dict(locals())
+
+ journalctl_options = journalctl_command_template.render(kwargs)
+ journalctl_options = re.sub(r'\s+', ' ', journalctl_options)
+ rc, output = rc_cmd(f'journalctl {journalctl_options}')
+ if raw:
+ # Each 'journalctl --output json' line is a separate JSON object
+ # So we should return list of dict
+ return [json.loads(line) for line in output.split('\n')]
+ return output
+
+
+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)
diff --git a/src/op_mode/memory.py b/src/op_mode/memory.py
index 178544be4..7666de646 100755
--- a/src/op_mode/memory.py
+++ b/src/op_mode/memory.py
@@ -20,7 +20,7 @@ import sys
import vyos.opmode
-def _get_system_memory():
+def _get_raw_data():
from re import search as re_search
def find_value(keyword, mem_data):
@@ -38,7 +38,7 @@ def _get_system_memory():
used = total - available
- res = {
+ mem_data = {
"total": total,
"free": available,
"used": used,
@@ -46,24 +46,21 @@ def _get_system_memory():
"cached": cached
}
- return res
-
-def _get_system_memory_human():
- from vyos.util import bytes_to_human
-
- mem = _get_system_memory()
-
- for key in mem:
+ for key in mem_data:
# The Linux kernel exposes memory values in kilobytes,
# so we need to normalize them
- mem[key] = bytes_to_human(mem[key], initial_exponent=10)
+ mem_data[key] = mem_data[key] * 1024
- return mem
-
-def _get_raw_data():
- return _get_system_memory_human()
+ return mem_data
def _get_formatted_output(mem):
+ from vyos.util import bytes_to_human
+
+ # For human-readable outputs, we convert bytes to more convenient units
+ # (100M, 1.3G...)
+ for key in mem:
+ mem[key] = bytes_to_human(mem[key])
+
out = "Total: {}\n".format(mem["total"])
out += "Free: {}\n".format(mem["free"])
out += "Used: {}".format(mem["used"])
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 845dbbb2c..f899eb3dc 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -22,12 +22,18 @@ import xmltodict
from sys import exit
from tabulate import tabulate
+from vyos.configquery import ConfigTreeQuery
+
from vyos.util import cmd
from vyos.util import dict_search
import vyos.opmode
+base = 'nat'
+unconf_message = 'NAT is not configured'
+
+
def _get_xml_translation(direction, family):
"""
Get conntrack XML output --src-nat|--dst-nat
@@ -277,6 +283,20 @@ def _get_formatted_translation(dict_data, nat_direction, family):
return output
+def _verify(func):
+ """Decorator checks if NAT config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+
+@_verify
def show_rules(raw: bool, direction: str, family: str):
nat_rules = _get_raw_data_rules(direction, family)
if raw:
@@ -285,6 +305,7 @@ def show_rules(raw: bool, direction: str, family: str):
return _get_formatted_output_rules(nat_rules, direction, family)
+@_verify
def show_statistics(raw: bool, direction: str, family: str):
nat_statistics = _get_raw_data_rules(direction, family)
if raw:
@@ -293,6 +314,7 @@ def show_statistics(raw: bool, direction: str, family: str):
return _get_formatted_output_statistics(nat_statistics, direction)
+@_verify
def show_translations(raw: bool, direction: str, family: str):
family = 'ipv6' if family == 'inet6' else 'ipv4'
nat_translation = _get_raw_translation(direction, family)
diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py
index 00992c66a..b21890728 100755
--- a/src/op_mode/openconnect.py
+++ b/src/op_mode/openconnect.py
@@ -31,14 +31,7 @@ occtl_socket = '/run/ocserv/occtl.socket'
def _get_raw_data_sessions():
rc, out = rc_cmd(f'sudo {occtl} --json --socket-file {occtl_socket} show users')
if rc != 0:
- output = {'openconnect':
- {
- 'configured': False,
- 'return_code': rc,
- 'reason': out
- }
- }
- return output
+ raise vyos.opmode.DataUnavailable(out)
sessions = json.loads(out)
return sessions
@@ -61,9 +54,8 @@ def _get_formatted_sessions(data):
def show_sessions(raw: bool):
config = ConfigTreeQuery()
- if not config.exists('vpn openconnect') and not raw:
- print('Openconnect is not configured')
- exit(0)
+ if not config.exists('vpn openconnect'):
+ raise vyos.opmode.UnconfiguredSubsystem('Openconnect is not configured')
openconnect_data = _get_raw_data_sessions()
if raw:
diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py
new file mode 100755
index 000000000..3797a7153
--- /dev/null
+++ b/src/op_mode/openvpn.py
@@ -0,0 +1,220 @@
+#!/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 os
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.util import bytes_to_human
+from vyos.util import commit_in_progress
+from vyos.util import call
+from vyos.config import Config
+
+def _get_tunnel_address(peer_host, peer_port, status_file):
+ peer = peer_host + ':' + peer_port
+ lst = []
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line in lines:
+ if peer in line:
+ lst.append(line)
+
+ # filter out subnet entries if iroute:
+ # in the case that one sets, say:
+ # [ ..., 'vtun10', 'server', 'client', 'client1', 'subnet','10.10.2.0/25']
+ # the status file will have an entry:
+ # 10.10.2.0/25,client1,...
+ lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
+
+ tunnel_ip = lst[0].split(',')[0]
+
+ return tunnel_ip
+
+def _get_interface_status(mode: str, interface: str) -> dict:
+ status_file = f'/run/openvpn/{interface}.status'
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local_host': '',
+ 'local_port': '',
+ 'date': '',
+ 'clients': [],
+ }
+
+ if not os.path.exists(status_file):
+ raise vyos.opmode.DataUnavailable('No information for interface {interface}')
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line_no, line in enumerate(lines):
+ # remove trailing newline character first
+ line = line.rstrip('\n')
+
+ # check first line header
+ if line_no == 0:
+ if mode == 'server':
+ if not line == 'OpenVPN CLIENT LIST':
+ raise vyos.opmode.InternalError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise vyos.opmode.InternalError('Expected "OpenVPN STATISTICS"')
+
+ continue
+
+ # second line informs us when the status file has been last updated
+ if line_no == 1:
+ data['date'] = line.lstrip('Updated,').rstrip('\n')
+ continue
+
+ if mode == 'server':
+ # for line_no > 1, lines appear as follows:
+ #
+ # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
+ # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
+ # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
+ # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
+ # ...
+ # ROUTING TABLE
+ # ...
+ if line_no >= 3:
+ # indicator that there are no more clients
+ if line == 'ROUTING TABLE':
+ break
+ # otherwise, get client data
+ remote = (line.split(',')[1]).rsplit(':', maxsplit=1)
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote_host': remote[0],
+ 'remote_port': remote[1],
+ 'tunnel': 'N/A',
+ 'rx_bytes': bytes_to_human(int(line.split(',')[2]),
+ precision=1),
+ 'tx_bytes': bytes_to_human(int(line.split(',')[3]),
+ precision=1),
+ 'online_since': line.split(',')[4]
+ }
+ client['tunnel'] = _get_tunnel_address(client['remote_host'],
+ client['remote_port'],
+ status_file)
+ data['clients'].append(client)
+ continue
+ else: # mode == 'client' or mode == 'site-to-site'
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote_host': 'N/A',
+ 'remote_port': 'N/A',
+ 'tunnel': 'N/A',
+ 'rx_bytes': bytes_to_human(int(line.split(',')[1]),
+ precision=1),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes_to_human(int(line.split(',')[1]),
+ precision=1)
+ data['clients'].append(client)
+ break
+
+ return data
+
+def _get_raw_data(mode: str) -> dict:
+ data = {}
+ conf = Config()
+ conf_dict = conf.get_config_dict(['interfaces', 'openvpn'],
+ get_first_key=True)
+ if not conf_dict:
+ return data
+
+ interfaces = [x for x in list(conf_dict) if conf_dict[x]['mode'] == mode]
+ for intf in interfaces:
+ data[intf] = _get_interface_status(mode, intf)
+ d = data[intf]
+ d['local_host'] = conf_dict[intf].get('local-host', '')
+ d['local_port'] = conf_dict[intf].get('local-port', '')
+ if mode in ['client', 'site-to-site']:
+ for client in d['clients']:
+ if 'shared-secret-key-file' in list(conf_dict[intf]):
+ client['name'] = 'None (PSK)'
+ client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0]
+ client['remote_port'] = conf_dict[intf].get('remote-port', '1194')
+
+ return data
+
+def _format_openvpn(data: dict) -> str:
+ if not data:
+ out = 'No OpenVPN interfaces configured'
+ return out
+
+ headers = ['Client CN', 'Remote Host', 'Tunnel IP', 'Local Host',
+ 'TX bytes', 'RX bytes', 'Connected Since']
+
+ out = ''
+ data_out = []
+ for intf in list(data):
+ l_host = data[intf]['local_host']
+ l_port = data[intf]['local_port']
+ for client in list(data[intf]['clients']):
+ r_host = client['remote_host']
+ r_port = client['remote_port']
+
+ out += f'\nOpenVPN status on {intf}\n\n'
+ name = client['name']
+ remote = r_host + ':' + r_port if r_host and r_port else 'N/A'
+ tunnel = client['tunnel']
+ local = l_host + ':' + l_port if l_host and l_port else 'N/A'
+ tx_bytes = client['tx_bytes']
+ rx_bytes = client['rx_bytes']
+ online_since = client['online_since']
+ data_out.append([name, remote, tunnel, local, tx_bytes,
+ rx_bytes, online_since])
+
+ out += tabulate(data_out, headers)
+
+ return out
+
+def show(raw: bool, mode: str) -> str:
+ openvpn_data = _get_raw_data(mode)
+
+ if raw:
+ return openvpn_data
+
+ return _format_openvpn(openvpn_data)
+
+def reset(interface: str):
+ if os.path.isfile(f'/run/openvpn/{interface}.conf'):
+ if commit_in_progress():
+ raise vyos.opmode.CommitInProgress('Retry OpenVPN reset: commit in progress.')
+ call(f'systemctl restart openvpn@{interface}.service')
+ else:
+ raise vyos.opmode.IncorrectValue(f'OpenVPN interface "{interface}" does not exist!')
+
+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)
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 60bbc0c78..610e63cb3 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -18,6 +18,25 @@ import os
import sys
import socket
import ipaddress
+from vyos.util import get_all_vrfs
+from vyos.ifconfig import Section
+
+
+def interface_list() -> list:
+ """
+ Get list of interfaces in system
+ :rtype: list
+ """
+ return Section.interfaces()
+
+
+def vrf_list() -> list:
+ """
+ Get list of VRFs in system
+ :rtype: list
+ """
+ return list(get_all_vrfs().keys())
+
options = {
'audible': {
@@ -63,6 +82,7 @@ options = {
'interface': {
'ping': '{command} -I {value}',
'type': '<interface>',
+ 'helpfunction': interface_list,
'help': 'Source interface'
},
'interval': {
@@ -128,6 +148,7 @@ options = {
'ping': 'sudo ip vrf exec {value} {command}',
'type': '<vrf>',
'help': 'Use specified VRF table',
+ 'helpfunction': vrf_list,
'dflt': 'default',
},
'verbose': {
@@ -142,20 +163,33 @@ ping = {
}
-class List (list):
- def first (self):
+class List(list):
+ def first(self):
return self.pop(0) if self else ''
def last(self):
return self.pop() if self else ''
- def prepend(self,value):
- self.insert(0,value)
+ def prepend(self, value):
+ self.insert(0, value)
+
+
+def completion_failure(option: str) -> None:
+ """
+ Shows failure message after TAB when option is wrong
+ :param option: failure option
+ :type str:
+ """
+ sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option))
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
def expension_failure(option, completions):
reason = 'Ambiguous' if completions else 'Invalid'
- sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ sys.stderr.write(
+ '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv),
+ option))
if completions:
sys.stderr.write(' Possible completions:\n ')
sys.stderr.write('\n '.join(completions))
@@ -196,28 +230,44 @@ if __name__ == '__main__':
if host == '--get-options':
args.first() # pop ping
args.first() # pop IP
+ usedoptionslist = []
while args:
- option = args.first()
-
- matched = complete(option)
+ option = args.first() # pop option
+ matched = complete(option) # get option parameters
+ usedoptionslist.append(option) # list of used options
+ # Select options
if not args:
+ # remove from Possible completions used options
+ for o in usedoptionslist:
+ if o in matched:
+ matched.remove(o)
sys.stdout.write(' '.join(matched))
sys.exit(0)
- if len(matched) > 1 :
+ if len(matched) > 1:
sys.stdout.write(' '.join(matched))
sys.exit(0)
+ # If option doesn't have value
+ if matched:
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+ else:
+ # Unexpected option
+ completion_failure(option)
- if options[matched[0]]['type'] == 'noarg':
- continue
-
- value = args.first()
+ value = args.first() # pop option's value
if not args:
matched = complete(option)
- sys.stdout.write(options[matched[0]]['type'])
+ helplines = options[matched[0]]['type']
+ # Run helpfunction to get list of possible values
+ if 'helpfunction' in options[matched[0]]:
+ result = options[matched[0]]['helpfunction']()
+ if result:
+ helplines = '\n' + ' '.join(result)
+ sys.stdout.write(helplines)
sys.exit(0)
- for name,option in options.items():
+ for name, option in options.items():
if 'dflt' in option and name not in args:
args.append(name)
args.append(option['dflt'])
@@ -234,8 +284,7 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'ping: Unknown host: {host}')
- command = convert(ping[version],args)
+ command = convert(ping[version], args)
# print(f'{command} {host}')
os.system(f'{command} {host}')
-
diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py
index 5be40082f..5953786f3 100755
--- a/src/op_mode/policy_route.py
+++ b/src/op_mode/policy_route.py
@@ -22,53 +22,13 @@ from vyos.config import Config
from vyos.util import cmd
from vyos.util import dict_search_args
-def get_policy_interfaces(conf, policy, name=None, ipv6=False):
- interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- routes = ['route', 'route6']
-
- def parse_if(ifname, if_conf):
- if 'policy' in if_conf:
- for route in routes:
- if route in if_conf['policy']:
- route_name = if_conf['policy'][route]
- name_str = f'({ifname},{route})'
-
- if not name:
- policy[route][route_name]['interface'].append(name_str)
- elif not ipv6 and name == route_name:
- policy['interface'].append(name_str)
-
- for iftype in ['vif', 'vif_s', 'vif_c']:
- if iftype in if_conf:
- for vifname, vif_conf in if_conf[iftype].items():
- parse_if(f'{ifname}.{vifname}', vif_conf)
-
- for iftype, iftype_conf in interfaces.items():
- for ifname, if_conf in iftype_conf.items():
- parse_if(ifname, if_conf)
-
-def get_config_policy(conf, name=None, ipv6=False, interfaces=True):
+def get_config_policy(conf, name=None, ipv6=False):
config_path = ['policy']
if name:
config_path += ['route6' if ipv6 else 'route', name]
policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
- if policy and interfaces:
- if name:
- policy['interface'] = []
- else:
- if 'route' in policy:
- for route_name, route_conf in policy['route'].items():
- route_conf['interface'] = []
-
- if 'route6' in policy:
- for route_name, route_conf in policy['route6'].items():
- route_conf['interface'] = []
-
- get_policy_interfaces(conf, policy, name, ipv6)
return policy
diff --git a/src/op_mode/route.py b/src/op_mode/route.py
index e1eee5bbf..d07a34180 100755
--- a/src/op_mode/route.py
+++ b/src/op_mode/route.py
@@ -54,6 +54,18 @@ frr_command_template = Template("""
{% endif %}
""")
+def show_summary(raw: bool):
+ from vyos.util import cmd
+
+ if raw:
+ from json import loads
+
+ output = cmd(f"vtysh -c 'show ip route summary json'")
+ return loads(output)
+ else:
+ output = cmd(f"vtysh -c 'show ip route summary'")
+ return output
+
def show(raw: bool,
family: str,
net: typing.Optional[str],
@@ -83,7 +95,12 @@ def show(raw: bool,
if raw:
from json import loads
- return loads(output)
+ d = loads(output)
+ collect = []
+ for k,_ in d.items():
+ for l in d[k]:
+ collect.append(l)
+ return collect
else:
return output
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index 9a5adcffb..e29e594a5 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -59,7 +59,11 @@ def get_vpn_tunnel_address(peer, interface):
for line in lines:
if peer in line:
lst.append(line)
- tunnel_ip = lst[1].split(',')[0]
+
+ # filter out subnet entries
+ lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
+
+ tunnel_ip = lst[0].split(',')[0]
return tunnel_ip
diff --git a/src/op_mode/storage.py b/src/op_mode/storage.py
index 75964c493..d16e271bd 100755
--- a/src/op_mode/storage.py
+++ b/src/op_mode/storage.py
@@ -20,6 +20,16 @@ import sys
import vyos.opmode
from vyos.util import cmd
+# FIY: As of coreutils from Debian Buster and Bullseye,
+# the outpt looks like this:
+#
+# $ df -h -t ext4 --output=source,size,used,avail,pcent
+# Filesystem Size Used Avail Use%
+# /dev/sda1 16G 7.6G 7.3G 51%
+#
+# Those field names are automatically normalized by vyos.opmode.run,
+# so we don't touch them here,
+# and only normalize values.
def _get_system_storage(only_persistent=False):
if not only_persistent:
@@ -32,11 +42,19 @@ def _get_system_storage(only_persistent=False):
return res
def _get_raw_data():
+ from re import sub as re_sub
+ from vyos.util import human_to_bytes
+
out = _get_system_storage(only_persistent=True)
lines = out.splitlines()
lists = [l.split() for l in lines]
res = {lists[0][i]: lists[1][i] for i in range(len(lists[0]))}
+ res["Size"] = human_to_bytes(res["Size"])
+ res["Used"] = human_to_bytes(res["Used"])
+ res["Avail"] = human_to_bytes(res["Avail"])
+ res["Use%"] = re_sub(r'%', '', res["Use%"])
+
return res
def _get_formatted_output():
diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py
index 4299d6e5f..6c7030ea0 100755
--- a/src/op_mode/traceroute.py
+++ b/src/op_mode/traceroute.py
@@ -18,6 +18,25 @@ import os
import sys
import socket
import ipaddress
+from vyos.util import get_all_vrfs
+from vyos.ifconfig import Section
+
+
+def interface_list() -> list:
+ """
+ Get list of interfaces in system
+ :rtype: list
+ """
+ return Section.interfaces()
+
+
+def vrf_list() -> list:
+ """
+ Get list of VRFs in system
+ :rtype: list
+ """
+ return list(get_all_vrfs().keys())
+
options = {
'backward-hops': {
@@ -48,6 +67,7 @@ options = {
'interface': {
'traceroute': '{command} -i {value}',
'type': '<interface>',
+ 'helpfunction': interface_list,
'help': 'Source interface'
},
'lookup-as': {
@@ -99,6 +119,7 @@ options = {
'traceroute': 'sudo ip vrf exec {value} {command}',
'type': '<vrf>',
'help': 'Use specified VRF table',
+ 'helpfunction': vrf_list,
'dflt': 'default'}
}
@@ -108,20 +129,33 @@ traceroute = {
}
-class List (list):
- def first (self):
+class List(list):
+ def first(self):
return self.pop(0) if self else ''
def last(self):
return self.pop() if self else ''
- def prepend(self,value):
- self.insert(0,value)
+ def prepend(self, value):
+ self.insert(0, value)
+
+
+def completion_failure(option: str) -> None:
+ """
+ Shows failure message after TAB when option is wrong
+ :param option: failure option
+ :type str:
+ """
+ sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option))
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
def expension_failure(option, completions):
reason = 'Ambiguous' if completions else 'Invalid'
- sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ sys.stderr.write(
+ '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv),
+ option))
if completions:
sys.stderr.write(' Possible completions:\n ')
sys.stderr.write('\n '.join(completions))
@@ -160,30 +194,46 @@ if __name__ == '__main__':
sys.exit("traceroute: Missing host")
if host == '--get-options':
- args.first() # pop traceroute
+ args.first() # pop ping
args.first() # pop IP
+ usedoptionslist = []
while args:
- option = args.first()
-
- matched = complete(option)
+ option = args.first() # pop option
+ matched = complete(option) # get option parameters
+ usedoptionslist.append(option) # list of used options
+ # Select options
if not args:
+ # remove from Possible completions used options
+ for o in usedoptionslist:
+ if o in matched:
+ matched.remove(o)
sys.stdout.write(' '.join(matched))
sys.exit(0)
- if len(matched) > 1 :
+ if len(matched) > 1:
sys.stdout.write(' '.join(matched))
sys.exit(0)
+ # If option doesn't have value
+ if matched:
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+ else:
+ # Unexpected option
+ completion_failure(option)
- if options[matched[0]]['type'] == 'noarg':
- continue
-
- value = args.first()
+ value = args.first() # pop option's value
if not args:
matched = complete(option)
- sys.stdout.write(options[matched[0]]['type'])
+ helplines = options[matched[0]]['type']
+ # Run helpfunction to get list of possible values
+ if 'helpfunction' in options[matched[0]]:
+ result = options[matched[0]]['helpfunction']()
+ if result:
+ helplines = '\n' + ' '.join(result)
+ sys.stdout.write(helplines)
sys.exit(0)
- for name,option in options.items():
+ for name, option in options.items():
if 'dflt' in option and name not in args:
args.append(name)
args.append(option['dflt'])
@@ -200,8 +250,7 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'traceroute: Unknown host: {host}')
- command = convert(traceroute[version],args)
+ command = convert(traceroute[version], args)
# print(f'{command} {host}')
os.system(f'{command} {host}')
-
diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py
index aeb50fe6e..a9a416761 100755
--- a/src/op_mode/vrf.py
+++ b/src/op_mode/vrf.py
@@ -31,14 +31,14 @@ def _get_raw_data(name=None):
If vrf name is set - get only this name data
If vrf name set and not found - return []
"""
- output = cmd('sudo ip --json --brief link show type vrf')
+ output = cmd('ip --json --brief link show type vrf')
data = json.loads(output)
if not data:
return []
if name:
is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False
if is_vrf_exists:
- output = cmd(f'sudo ip --json --brief link show dev {name}')
+ output = cmd(f'ip --json --brief link show dev {name}')
data = json.loads(output)
return data
return []
@@ -51,7 +51,7 @@ def _get_vrf_members(vrf: str) -> list:
:param vrf: str
:return: list
"""
- output = cmd(f'sudo ip --json --brief link show master {vrf}')
+ output = cmd(f'ip --json --brief link show master {vrf}')
answer = json.loads(output)
interfaces = []
for data in answer:
diff --git a/src/op_mode/webproxy_update_blacklist.sh b/src/op_mode/webproxy_update_blacklist.sh
index 43a4b79fc..4fb9a54c6 100755
--- a/src/op_mode/webproxy_update_blacklist.sh
+++ b/src/op_mode/webproxy_update_blacklist.sh
@@ -18,6 +18,23 @@ blacklist_url='ftp://ftp.univ-tlse1.fr/pub/reseau/cache/squidguard_contrib/black
data_dir="/opt/vyatta/etc/config/url-filtering"
archive="${data_dir}/squidguard/archive"
db_dir="${data_dir}/squidguard/db"
+conf_file="/etc/squidguard/squidGuard.conf"
+tmp_conf_file="/tmp/sg_update_db.conf"
+
+#$1-category
+#$2-type
+#$3-list
+create_sg_db ()
+{
+ FILE=$db_dir/$1/$2
+ if test -f "$FILE"; then
+ rm -f ${tmp_conf_file}
+ printf "dbhome $db_dir\ndest $1 {\n $3 $1/$2\n}\nacl {\n default {\n pass any\n }\n}" >> ${tmp_conf_file}
+ /usr/bin/squidGuard -b -c ${tmp_conf_file} -C $FILE
+ rm -f ${tmp_conf_file}
+ fi
+
+}
while [ $# -gt 0 ]
do
@@ -88,7 +105,17 @@ if [[ -n $update ]] && [[ $update -eq "yes" ]]; then
# fix permissions
chown -R proxy:proxy ${db_dir}
- chmod 2770 ${db_dir}
+
+ #create db
+ category_list=(`find $db_dir -type d -exec basename {} \; `)
+ for category in ${category_list[@]}
+ do
+ create_sg_db $category "domains" "domainlist"
+ create_sg_db $category "urls" "urllist"
+ create_sg_db $category "expressions" "expressionlist"
+ done
+ chown -R proxy:proxy ${db_dir}
+ chmod 755 ${db_dir}
logger --priority WARNING "webproxy blacklist entries updated (${count_before}/${count_after})"