summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/completion/list_login_ttys.py25
-rwxr-xr-xsrc/conf_mode/firewall.py70
-rwxr-xr-xsrc/conf_mode/interfaces_geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces_l2tpv3.py2
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py30
-rwxr-xr-xsrc/conf_mode/interfaces_vti.py2
-rwxr-xr-xsrc/conf_mode/interfaces_vxlan.py31
-rwxr-xr-xsrc/conf_mode/interfaces_wireless.py84
-rwxr-xr-xsrc/conf_mode/policy_route.py29
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py3
-rwxr-xr-xsrc/conf_mode/service_snmp.py13
-rwxr-xr-xsrc/conf_mode/system_conntrack.py21
-rwxr-xr-xsrc/conf_mode/system_console.py15
-rwxr-xr-xsrc/conf_mode/system_option.py9
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py16
-rwxr-xr-xsrc/conf_mode/vrf.py23
-rw-r--r--src/etc/bash_completion.d/vyatta-op685
-rw-r--r--src/etc/default/vyatta217
-rwxr-xr-xsrc/helpers/vyos_net_name143
-rwxr-xr-xsrc/migration-scripts/firewall/16-to-1760
-rw-r--r--src/migration-scripts/firewall/7-to-88
-rw-r--r--src/migration-scripts/nat/6-to-72
-rw-r--r--src/migration-scripts/openvpn/1-to-28
-rw-r--r--src/migration-scripts/openvpn/2-to-339
-rw-r--r--src/migration-scripts/openvpn/3-to-426
-rwxr-xr-xsrc/op_mode/bridge.py2
-rwxr-xr-xsrc/op_mode/connect_disconnect.py22
-rwxr-xr-xsrc/op_mode/dhcp.py2
-rwxr-xr-xsrc/op_mode/interfaces.py20
-rwxr-xr-xsrc/op_mode/ipsec.py490
-rwxr-xr-xsrc/op_mode/nat.py56
-rwxr-xr-xsrc/op_mode/openconnect.py6
-rwxr-xr-xsrc/op_mode/pki.py3
-rwxr-xr-xsrc/op_mode/powerctrl.py7
-rwxr-xr-xsrc/op_mode/restart.py127
-rw-r--r--src/op_mode/serial.py38
-rwxr-xr-xsrc/op_mode/ssh.py2
-rw-r--r--src/op_mode/tech_support.py394
-rw-r--r--src/op_mode/zone.py4
-rwxr-xr-xsrc/opt/vyatta/bin/restricted-shell11
-rwxr-xr-xsrc/opt/vyatta/bin/vyatta-op-cmd-wrapper6
-rw-r--r--src/opt/vyatta/etc/LICENSE340
-rw-r--r--src/opt/vyatta/etc/shell/level/users/allowed-op20
-rw-r--r--src/opt/vyatta/etc/shell/level/users/allowed-op.in16
-rwxr-xr-xsrc/opt/vyatta/sbin/if-mib-alias130
-rwxr-xr-xsrc/opt/vyatta/sbin/vyos-persistpath19
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common82
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run240
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv97
-rw-r--r--src/services/api/graphql/session/errors/op_mode_errors.py2
-rwxr-xr-xsrc/services/vyos-configd16
-rwxr-xr-xsrc/services/vyos-conntrack-logger458
-rw-r--r--src/systemd/vyos-conntrack-logger.service21
53 files changed, 3861 insertions, 333 deletions
diff --git a/src/completion/list_login_ttys.py b/src/completion/list_login_ttys.py
new file mode 100644
index 000000000..4d77a1b8b
--- /dev/null
+++ b/src/completion/list_login_ttys.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 vyos.utils.serial import get_serial_units
+
+if __name__ == '__main__':
+ # Autocomplete uses runtime state rather than the config tree, as a manual
+ # restart/cleanup may be needed for deleted devices.
+ tty_completions = [ '<text>' ] + [ x['device'] for x in get_serial_units() if 'device' in x ]
+ print(' '.join(tty_completions))
+
+
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index ec6b86ef2..352d5cbb1 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -128,7 +128,49 @@ def get_config(config=None):
return firewall
-def verify_rule(firewall, rule_conf, ipv6):
+def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False):
+ targets_seen = []
+ targets_pending = [jump_target]
+
+ while targets_pending:
+ target = targets_pending.pop()
+
+ if not ipv6:
+ if target not in dict_search_args(firewall, 'ipv4', 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ target_rules = dict_search_args(firewall, 'ipv4', 'name', target, 'rule')
+ else:
+ if target not in dict_search_args(firewall, 'ipv6', 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
+ target_rules = dict_search_args(firewall, 'ipv6', 'name', target, 'rule')
+
+ no_ipsec_in = root_chain in ('output', )
+
+ if target_rules:
+ for target_rule_conf in target_rules.values():
+ # Output hook types will not tolerate 'meta ipsec exists' matches even in jump targets:
+ if no_ipsec_in and (dict_search_args(target_rule_conf, 'ipsec', 'match_ipsec_in') is not None \
+ or dict_search_args(target_rule_conf, 'ipsec', 'match_none_in') is not None):
+ if not ipv6:
+ raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall name {target} rules contain incompatible ipsec inbound matches')
+ else:
+ raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall ipv6 name {target} rules contain incompatible ipsec inbound matches')
+ # Make sure we're not looping back on ourselves somewhere:
+ if recursive and 'jump_target' in target_rule_conf:
+ child_target = target_rule_conf['jump_target']
+ if child_target in targets_seen:
+ if not ipv6:
+ raise ConfigError(f'Loop detected in jump-targets, firewall name {target} refers to previously traversed name {child_target}')
+ else:
+ raise ConfigError(f'Loop detected in jump-targets, firewall ipv6 name {target} refers to previously traversed ipv6 name {child_target}')
+ targets_pending.append(child_target)
+ if len(targets_seen) == 7:
+ path_txt = ' -> '.join(targets_seen)
+ Warning(f'Deep nesting of jump targets has reached 8 levels deep, following the path {path_txt} -> {child_target}!')
+
+ targets_seen.append(target)
+
+def verify_rule(firewall, chain_name, rule_conf, ipv6):
if 'action' not in rule_conf:
raise ConfigError('Rule action must be defined')
@@ -139,12 +181,10 @@ def verify_rule(firewall, rule_conf, ipv6):
if 'jump' not in rule_conf['action']:
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
- if not ipv6:
- if target not in dict_search_args(firewall, 'ipv4', 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ if chain_name != 'name': # This is a bit clumsy, but consolidates a chunk of code.
+ verify_jump_target(firewall, chain_name, target, ipv6, recursive=True)
else:
- if target not in dict_search_args(firewall, 'ipv6', 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
+ verify_jump_target(firewall, chain_name, target, ipv6, recursive=False)
if rule_conf['action'] == 'offload':
if 'offload_target' not in rule_conf:
@@ -185,8 +225,10 @@ def verify_rule(firewall, rule_conf, ipv6):
raise ConfigError('Limit rate integer cannot be less than 1')
if 'ipsec' in rule_conf:
- if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']):
- raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"')
+ if {'match_ipsec_in', 'match_none_in'} <= set(rule_conf['ipsec']):
+ raise ConfigError('Cannot specify both "match-ipsec" and "match-none"')
+ if {'match_ipsec_out', 'match_none_out'} <= set(rule_conf['ipsec']):
+ raise ConfigError('Cannot specify both "match-ipsec" and "match-none"')
if 'recent' in rule_conf:
if not {'count', 'time'} <= set(rule_conf['recent']):
@@ -349,13 +391,11 @@ def verify(firewall):
raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
if name_conf['default_jump_target'] == name_id:
raise ConfigError(f'Loop detected on default-jump-target.')
- ## Now need to check that default-jump-target exists (other firewall chain/name)
- if target not in dict_search_args(firewall['ipv4'], 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ verify_jump_target(firewall, name, target, False, recursive=True)
if 'rule' in name_conf:
for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, rule_conf, False)
+ verify_rule(firewall, name, rule_conf, False)
if 'ipv6' in firewall:
for name in ['name','forward','input','output', 'prerouting']:
@@ -369,13 +409,11 @@ def verify(firewall):
raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
if name_conf['default_jump_target'] == name_id:
raise ConfigError(f'Loop detected on default-jump-target.')
- ## Now need to check that default-jump-target exists (other firewall chain/name)
- if target not in dict_search_args(firewall['ipv6'], 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ verify_jump_target(firewall, name, target, True, recursive=True)
if 'rule' in name_conf:
for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, rule_conf, True)
+ verify_rule(firewall, name, rule_conf, True)
#### ZONESSSS
local_zone = False
diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py
index 769139e0f..007708d4a 100755
--- a/src/conf_mode/interfaces_geneve.py
+++ b/src/conf_mode/interfaces_geneve.py
@@ -24,6 +24,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
+from vyos.configverify import verify_vrf
from vyos.ifconfig import GeneveIf
from vyos.utils.network import interface_exists
from vyos import ConfigError
@@ -59,6 +60,7 @@ def verify(geneve):
verify_mtu_ipv6(geneve)
verify_address(geneve)
+ verify_vrf(geneve)
verify_bond_bridge_member(geneve)
verify_mirror_redirect(geneve)
diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py
index e25793543..b9f827bee 100755
--- a/src/conf_mode/interfaces_l2tpv3.py
+++ b/src/conf_mode/interfaces_l2tpv3.py
@@ -24,6 +24,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
+from vyos.configverify import verify_vrf
from vyos.ifconfig import L2TPv3If
from vyos.utils.kernel import check_kmod
from vyos.utils.network import is_addr_assigned
@@ -76,6 +77,7 @@ def verify(l2tpv3):
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
+ verify_vrf(l2tpv3)
verify_bond_bridge_member(l2tpv3)
verify_mirror_redirect(l2tpv3)
return None
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 017010a61..a03bd5959 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -235,10 +235,6 @@ def verify_pki(openvpn):
def verify(openvpn):
if 'deleted' in openvpn:
- # remove totp secrets file if totp is not configured
- if os.path.isfile(otp_file.format(**openvpn)):
- os.remove(otp_file.format(**openvpn))
-
verify_bridge_delete(openvpn)
return None
@@ -326,8 +322,8 @@ def verify(openvpn):
if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]:
raise ConfigError('Must specify IPv4 "subnet-mask" for local-address')
- if dict_search('encryption.ncp_ciphers', openvpn):
- raise ConfigError('NCP ciphers can only be used in client or server mode')
+ if dict_search('encryption.data_ciphers', openvpn):
+ raise ConfigError('Cipher negotiation can only be used in client or server mode')
else:
# checks for client-server or site-to-site bridged
@@ -432,6 +428,13 @@ def verify(openvpn):
if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
+ if 'topology' in openvpn['server']:
+ if openvpn['server']['topology'] == 'net30':
+ DeprecationWarning('Topology net30 is deprecated '\
+ 'and will be removed in future VyOS versions. '\
+ 'Switch to "subnet" or "p2p"'
+ )
+
# add mfa users to the file the mfa plugin uses
if dict_search('server.mfa.totp', openvpn):
user_data = ''
@@ -517,7 +520,7 @@ def verify(openvpn):
if dict_search('encryption.cipher', openvpn):
raise ConfigError('"encryption cipher" option is deprecated for TLS mode. '
- 'Use "encryption ncp-ciphers" instead')
+ 'Use "encryption data-ciphers" instead')
if dict_search('encryption.cipher', openvpn) == 'none':
print('Warning: "encryption none" was specified!')
@@ -628,9 +631,19 @@ def generate_pki_files(openvpn):
def generate(openvpn):
+ if 'deleted' in openvpn:
+ # remove totp secrets file if totp is not configured
+ if os.path.isfile(otp_file.format(**openvpn)):
+ os.remove(otp_file.format(**openvpn))
+ return None
+
+ if 'disable' in openvpn:
+ return None
+
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
openvpn['plugin_dir'] = '/usr/lib/openvpn'
+
# create base config directory on demand
makedir(directory, user, group)
# enforce proper permissions on /run/openvpn
@@ -647,9 +660,6 @@ def generate(openvpn):
if os.path.isdir(service_dir):
rmtree(service_dir, ignore_errors=True)
- if 'deleted' in openvpn or 'disable' in openvpn:
- return None
-
# create client config directory on demand
makedir(ccd_dir, user, group)
diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py
index e6a833df7..20629c6c1 100755
--- a/src/conf_mode/interfaces_vti.py
+++ b/src/conf_mode/interfaces_vti.py
@@ -19,6 +19,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_vrf
from vyos.ifconfig import VTIIf
from vyos import ConfigError
from vyos import airbag
@@ -38,6 +39,7 @@ def get_config(config=None):
return vti
def verify(vti):
+ verify_vrf(vti)
verify_mirror_redirect(vti)
return None
diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
index 39365968a..68646e8ff 100755
--- a/src/conf_mode/interfaces_vxlan.py
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_bond_bridge_member
+from vyos.configverify import verify_vrf
from vyos.ifconfig import Interface
from vyos.ifconfig import VXLANIf
from vyos.template import is_ipv6
@@ -178,13 +179,36 @@ def verify(vxlan):
'is member of a bridge interface!')
vnis_used = []
+ vlans_used = []
for vif, vif_config in vxlan['vlan_to_vni'].items():
if 'vni' not in vif_config:
raise ConfigError(f'Must define VNI for VLAN "{vif}"!')
vni = vif_config['vni']
- if vni in vnis_used:
- raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')
- vnis_used.append(vni)
+
+ err_msg = f'VLAN range "{vif}" does not match VNI range "{vni}"!'
+ vif_range, vni_range = list(map(int, vif.split('-'))), list(map(int, vni.split('-')))
+
+ if len(vif_range) != len(vni_range):
+ raise ConfigError(err_msg)
+
+ if len(vif_range) > 1:
+ if vni_range[0] > vni_range[-1] or vif_range[0] > vif_range[-1]:
+ raise ConfigError('The upper bound of the range must be greater than the lower bound!')
+ vni_range = range(vni_range[0], vni_range[1] + 1)
+ vif_range = range(vif_range[0], vif_range[1] + 1)
+
+ if len(vif_range) != len(vni_range):
+ raise ConfigError(err_msg)
+
+ for vni_id in vni_range:
+ if vni_id in vnis_used:
+ raise ConfigError(f'VNI "{vni_id}" is already assigned to a different VLAN!')
+ vnis_used.append(vni_id)
+
+ for vif_id in vif_range:
+ if vif_id in vlans_used:
+ raise ConfigError(f'VLAN "{vif_id}" is already in use!')
+ vlans_used.append(vif_id)
if dict_search('parameters.neighbor_suppress', vxlan) != None:
if 'is_bridge_member' not in vxlan:
@@ -193,6 +217,7 @@ def verify(vxlan):
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
+ verify_vrf(vxlan)
verify_bond_bridge_member(vxlan)
verify_mirror_redirect(vxlan)
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
index 73944dc8b..f35a250cb 100755
--- a/src/conf_mode/interfaces_wireless.py
+++ b/src/conf_mode/interfaces_wireless.py
@@ -19,6 +19,7 @@ import os
from sys import exit
from re import findall
from netaddr import EUI, mac_unix_expanded
+from time import sleep
from vyos.config import Config
from vyos.configdict import get_interface_dict
@@ -34,6 +35,9 @@ from vyos.template import render
from vyos.utils.dict import dict_search
from vyos.utils.kernel import check_kmod
from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.network import interface_exists
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -93,6 +97,11 @@ def get_config(config=None):
if wifi.from_defaults(['security', 'wpa']): # if not set by user
del wifi['security']['wpa']
+ # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number
+ if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []):
+ wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable']
+ del wifi['capabilities']['ht']['40mhz_incapable']
+
if dict_search('security.wpa', wifi) != None:
wpa_cipher = wifi['security']['wpa'].get('cipher')
wpa_mode = wifi['security']['wpa'].get('mode')
@@ -120,7 +129,7 @@ def get_config(config=None):
tmp = find_other_stations(conf, base, wifi['ifname'])
if tmp: wifi['station_interfaces'] = tmp
- # used in hostapt.conf.j2
+ # used in hostapd.conf.j2
wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi)
wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi)
@@ -184,11 +193,18 @@ def verify(wifi):
if not any(i in ['passphrase', 'radius'] for i in wpa):
raise ConfigError('Misssing WPA key or RADIUS server')
+ if 'username' in wpa:
+ if 'passphrase' not in wpa:
+ raise ConfigError('WPA-Enterprise configured - missing passphrase!')
+ elif 'passphrase' in wpa:
+ # check if passphrase meets the regex .{8,63}
+ if len(wpa['passphrase']) < 8 or len(wpa['passphrase']) > 63:
+ raise ConfigError('WPA passphrase must be between 8 and 63 characters long')
if 'radius' in wpa:
if 'server' in wpa['radius']:
for server in wpa['radius']['server']:
if 'key' not in wpa['radius']['server'][server]:
- raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}')
+ raise ConfigError(f'Missing RADIUS shared secret key for server: {server}')
if 'capabilities' in wifi:
capabilities = wifi['capabilities']
@@ -225,11 +241,6 @@ def verify(wifi):
def generate(wifi):
interface = wifi['ifname']
- # always stop hostapd service first before reconfiguring it
- call(f'systemctl stop hostapd@{interface}.service')
- # always stop wpa_supplicant service first before reconfiguring it
- call(f'systemctl stop wpa_supplicant@{interface}.service')
-
# Delete config files if interface is removed
if 'deleted' in wifi:
if os.path.isfile(hostapd_conf.format(**wifi)):
@@ -265,11 +276,6 @@ def generate(wifi):
mac.dialect = mac_unix_expanded
wifi['mac'] = str(mac)
- # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number
- if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []):
- wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable']
- del wifi['capabilities']['ht']['40mhz_incapable']
-
# render appropriate new config files depending on access-point or station mode
if wifi['type'] == 'access-point':
render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi)
@@ -283,23 +289,45 @@ def generate(wifi):
def apply(wifi):
interface = wifi['ifname']
+ # From systemd source code:
+ # If there's a stop job queued before we enter the DEAD state, we shouldn't act on Restart=,
+ # in order to not undo what has already been enqueued. */
+ #
+ # It was found that calling restart on hostapd will (4 out of 10 cases) deactivate
+ # the service instead of restarting it, when it was not yet properly stopped
+ # systemd[1]: hostapd@wlan1.service: Deactivated successfully.
+ # Thus kill all WIFI service and start them again after it's ensured nothing lives
+ call(f'systemctl stop hostapd@{interface}.service')
+ call(f'systemctl stop wpa_supplicant@{interface}.service')
+
if 'deleted' in wifi:
- WiFiIf(interface).remove()
- else:
- # Finally create the new interface
- w = WiFiIf(**wifi)
- w.update(wifi)
-
- # Enable/Disable interface - interface is always placed in
- # administrative down state in WiFiIf class
- if 'disable' not in wifi:
- # Physical interface is now configured. Proceed by starting hostapd or
- # wpa_supplicant daemon. When type is monitor we can just skip this.
- if wifi['type'] == 'access-point':
- call(f'systemctl start hostapd@{interface}.service')
-
- elif wifi['type'] == 'station':
- call(f'systemctl start wpa_supplicant@{interface}.service')
+ WiFiIf(**wifi).remove()
+ return None
+
+ while (is_systemd_service_running(f'hostapd@{interface}.service') or \
+ is_systemd_service_active(f'hostapd@{interface}.service')):
+ sleep(0.250) # wait 250ms
+
+ # Finally create the new interface
+ w = WiFiIf(**wifi)
+ w.update(wifi)
+
+ # Enable/Disable interface - interface is always placed in
+ # administrative down state in WiFiIf class
+ if 'disable' not in wifi:
+ # Wait until interface was properly added to the Kernel
+ ii = 0
+ while not (interface_exists(interface) and ii < 20):
+ sleep(0.250) # wait 250ms
+ ii += 1
+
+ # Physical interface is now configured. Proceed by starting hostapd or
+ # wpa_supplicant daemon. When type is monitor we can just skip this.
+ if wifi['type'] == 'access-point':
+ call(f'systemctl start hostapd@{interface}.service')
+
+ elif wifi['type'] == 'station':
+ call(f'systemctl start wpa_supplicant@{interface}.service')
return None
diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py
index c58fe1bce..223175b8a 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -25,6 +25,9 @@ from vyos.template import render
from vyos.utils.dict import dict_search_args
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -83,6 +86,9 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
if not tcp_flags or 'syn' not in tcp_flags:
raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+ if 'vrf' in rule_conf['set'] and 'table' in rule_conf['set']:
+ raise ConfigError(f'{name} rule {rule_id}: Cannot set both forwarding route table and VRF')
+
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
if tcp_flags:
if dict_search_args(rule_conf, 'protocol') != 'tcp':
@@ -152,15 +158,26 @@ def apply_table_marks(policy):
for name, pol_conf in policy[route].items():
if 'rule' in pol_conf:
for rule_id, rule_conf in pol_conf['rule'].items():
+ vrf_table_id = None
set_table = dict_search_args(rule_conf, 'set', 'table')
- if set_table:
+ set_vrf = dict_search_args(rule_conf, 'set', 'vrf')
+ if set_vrf:
+ if set_vrf == 'default':
+ vrf_table_id = rt_global_vrf
+ else:
+ vrf_table_id = get_vrf_tableid(set_vrf)
+ elif set_table:
if set_table == 'main':
- set_table = '254'
- if set_table in tables:
+ vrf_table_id = rt_global_table
+ else:
+ vrf_table_id = set_table
+ if vrf_table_id is not None:
+ vrf_table_id = int(vrf_table_id)
+ if vrf_table_id in tables:
continue
- tables.append(set_table)
- table_mark = mark_offset - int(set_table)
- cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
+ tables.append(vrf_table_id)
+ table_mark = mark_offset - vrf_table_id
+ cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}')
def cleanup_table_marks():
for cmd_str in ['ip', 'ip -6']:
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 9455b6109..db870aae5 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -86,7 +86,8 @@ def get_config(config=None):
monitoring['custom_scripts_dir'] = custom_scripts_dir
monitoring['hostname'] = get_hostname()
monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False)
- monitoring['nft_chains'] = get_nft_filter_chains()
+ if conf.exists('firewall'):
+ monitoring['nft_chains'] = get_nft_filter_chains()
# Redefine azure group-metrics 'single-table' and 'table-per-metric'
if 'azure_data_explorer' in monitoring:
diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py
index 6f025cc23..c9c0ed9a0 100755
--- a/src/conf_mode/service_snmp.py
+++ b/src/conf_mode/service_snmp.py
@@ -41,6 +41,7 @@ config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
+default_script_dir = r'/config/user-data/'
systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf'
systemd_service = 'snmpd.service'
@@ -85,8 +86,20 @@ def get_config(config=None):
tmp = {'::1': {'port': '161'}}
snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
+ if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']:
+ for key, val in snmp['script_extensions']['extension_name'].items():
+ if 'script' not in val:
+ continue
+ script_path = val['script']
+ # if script has not absolute path, use pre configured path
+ if not os.path.isabs(script_path):
+ script_path = os.path.join(default_script_dir, script_path)
+
+ snmp['script_extensions']['extension_name'][key]['script'] = script_path
+
return snmp
+
def verify(snmp):
if 'deleted' in snmp:
return None
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py
index aa290788c..2529445bf 100755
--- a/src/conf_mode/system_conntrack.py
+++ b/src/conf_mode/system_conntrack.py
@@ -13,7 +13,7 @@
#
# 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 os
from sys import exit
@@ -24,7 +24,8 @@ from vyos.configdep import set_dependents, call_dependents
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
-from vyos.utils.process import cmd
+from vyos.utils.file import write_file
+from vyos.utils.process import cmd, call
from vyos.utils.process import rc_cmd
from vyos.template import render
from vyos import ConfigError
@@ -34,6 +35,7 @@ airbag.enable()
conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
nftables_ct_file = r'/run/nftables-ct.conf'
+vyos_conntrack_logger_config = r'/run/vyos-conntrack-logger.conf'
# Every ALG (Application Layer Gateway) consists of either a Kernel Object
# also called a Kernel Module/Driver or some rules present in iptables
@@ -113,6 +115,7 @@ def get_config(config=None):
return conntrack
+
def verify(conntrack):
for inet in ['ipv4', 'ipv6']:
if dict_search_args(conntrack, 'ignore', inet, 'rule') != None:
@@ -181,6 +184,11 @@ def generate(conntrack):
if not os.path.exists(nftables_ct_file):
conntrack['first_install'] = True
+ if 'log' not in conntrack:
+ # Remove old conntrack-logger config and return
+ if os.path.exists(vyos_conntrack_logger_config):
+ os.unlink(vyos_conntrack_logger_config)
+
# Determine if conntrack is needed
conntrack['ipv4_firewall_action'] = 'return'
conntrack['ipv6_firewall_action'] = 'return'
@@ -199,6 +207,11 @@ def generate(conntrack):
render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
+
+ if 'log' in conntrack:
+ log_conf_json = json.dumps(conntrack['log'], indent=4)
+ write_file(vyos_conntrack_logger_config, log_conf_json)
+
return None
def apply(conntrack):
@@ -243,8 +256,12 @@ def apply(conntrack):
# See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
cmd(f'sysctl -f {sysctl_file}')
+ if 'log' in conntrack:
+ call(f'systemctl restart vyos-conntrack-logger.service')
+
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 19bbb8875..27bf92e0b 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -19,8 +19,10 @@ from pathlib import Path
from vyos.config import Config
from vyos.utils.process import call
+from vyos.utils.serial import restart_login_consoles
from vyos.system import grub_util
from vyos.template import render
+from vyos.defaults import directories
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -74,7 +76,6 @@ def generate(console):
for root, dirs, files in os.walk(base_dir):
for basename in files:
if 'serial-getty' in basename:
- call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
if not console or 'device' not in console:
@@ -122,6 +123,11 @@ def apply(console):
# Reload systemd manager configuration
call('systemctl daemon-reload')
+ # Service control moved to vyos.utils.serial to unify checks and prompts.
+ # If users are connected, we want to show an informational message on completing
+ # the process, but not halt configuration processing with an interactive prompt.
+ restart_login_consoles(prompt_user=False, quiet=False)
+
if not console:
return None
@@ -129,13 +135,6 @@ def apply(console):
# Configure screen blank powersaving on VGA console
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
- # Start getty process on configured serial interfaces
- for device in console['device']:
- # Only start console if it exists on the running system. If a user
- # detaches a USB serial console and reboots - it should not fail!
- if os.path.exists(f'/dev/{device}'):
- call(f'systemctl restart serial-getty@{device}.service')
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index 571ce55ec..180686924 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -31,6 +31,7 @@ from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_running
from vyos.utils.network import is_addr_assigned
from vyos.utils.network import is_intf_addr_assigned
+from vyos.configdep import set_dependents, call_dependents
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -55,6 +56,12 @@ def get_config(config=None):
get_first_key=True,
with_recursive_defaults=True)
+ if 'performance' in options:
+ # Update IPv4 and IPv6 options after TuneD reapplies
+ # sysctl from config files
+ for protocol in ['ip', 'ipv6']:
+ set_dependents(protocol, conf)
+
return options
def verify(options):
@@ -145,6 +152,8 @@ def apply(options):
else:
cmd('systemctl stop tuned.service')
+ call_dependents()
+
# Keyboard layout - there will be always the default key inside the dict
# but we check for key existence anyway
if 'keyboard_layout' in options:
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index dc78c755e..cf82b767f 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -24,6 +24,7 @@ from time import sleep
from vyos.base import Warning
from vyos.config import Config
+from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
from vyos.configdep import call_dependents
from vyos.configdict import leaf_node_changed
@@ -86,9 +87,22 @@ def get_config(config=None):
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True,
- with_recursive_defaults=True,
with_pki=True)
+ # We have to cleanup the default dict, as default values could
+ # enable features which are not explicitly enabled on the
+ # CLI. E.g. dead-peer-detection defaults should not be injected
+ # unless the feature is explicitly opted in to by setting the
+ # top-level node
+ default_values = conf.get_config_defaults(**ipsec.kwargs, recursive=True)
+
+ if 'ike_group' in ipsec:
+ for name, ike in ipsec['ike_group'].items():
+ if 'dead_peer_detection' not in ike:
+ del default_values['ike_group'][name]['dead_peer_detection']
+
+ ipsec = config_dict_merge(default_values, ipsec)
+
ipsec['dhcp_interfaces'] = set()
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 8d8c234c0..72b178c89 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+from jmespath import search
from json import loads
from vyos.config import Config
@@ -26,7 +27,7 @@ from vyos.ifconfig import Interface
from vyos.template import render
from vyos.template import render_to_string
from vyos.utils.dict import dict_search
-from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import get_vrf_members
from vyos.utils.network import interface_exists
from vyos.utils.process import call
@@ -70,6 +71,14 @@ def has_rule(af : str, priority : int, table : str=None):
return True
return False
+def is_nft_vrf_zone_rule_setup() -> bool:
+ """
+ Check if an nftables connection tracking rule already exists
+ """
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ return bool(num_rules)
+
def vrf_interfaces(c, match):
matched = []
old_level = c.get_level()
@@ -160,8 +169,8 @@ def verify(vrf):
# routing table id can't be changed - OS restriction
if interface_exists(name):
- tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name)))
- if tmp and tmp != vrf_config['table']:
+ tmp = get_vrf_tableid(name)
+ if tmp and tmp != int(vrf_config['table']):
raise ConfigError(f'VRF "{name}" table id modification not possible!')
# VRF routing table ID must be unique on the system
@@ -264,6 +273,7 @@ def apply(vrf):
if not has_rule(afi, 2000, 'l3mdev'):
call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
+ nft_vrf_zone_rule_setup = False
for name, config in vrf['name'].items():
table = config['table']
if not interface_exists(name):
@@ -302,7 +312,12 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
- if vrf['conntrack']:
+ # Only call into nftables as long as there is nothing setup to avoid wasting
+ # CPU time and thus lenghten the commit process
+ if not nft_vrf_zone_rule_setup:
+ nft_vrf_zone_rule_setup = is_nft_vrf_zone_rule_setup()
+ # Install nftables conntrack rules only once
+ if vrf['conntrack'] and not nft_vrf_zone_rule_setup:
for chain, rule in nftables_rules.items():
cmd(f'nft add rule inet vrf_zones {chain} {rule}')
diff --git a/src/etc/bash_completion.d/vyatta-op b/src/etc/bash_completion.d/vyatta-op
new file mode 100644
index 000000000..8ac2d9b20
--- /dev/null
+++ b/src/etc/bash_completion.d/vyatta-op
@@ -0,0 +1,685 @@
+# vyatta bash operational mode completion
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Tom Grennan
+# Date: 2007
+# Description: setup bash completion for Vyatta operational commands
+#
+# **** End License ****
+
+test -z "$_vyatta_less_options" && \
+ declare -r _vyatta_less_options="\
+ --QUIT-AT-EOF\
+ --quit-if-one-screen\
+ --RAW-CONTROL-CHARS\
+ --squeeze-blank-lines\
+ --no-init"
+test -z "$_vyatta_default_pager" && \
+ declare -r _vyatta_default_pager="less \
+ --buffers=64\
+ --auto-buffers\
+ --no-lessopen\
+ $_vyatta_less_options"
+test -z "$VYATTA_PAGER" && \
+ declare -x VYATTA_PAGER=$_vyatta_default_pager
+
+_vyatta_op_do_key_bindings ()
+{
+ if [[ "$SHELL" != "/bin/vbash" && "$SHELL" != "/sbin/radius_shell" ]]; then
+ # only do bindings if vbash and radius_shell
+ return
+ fi
+ nullglob_save=$(shopt -p nullglob)
+ shopt -u nullglob
+ case "$-" in
+ *i*)
+ bind '"?": possible-completions'
+ bind 'set show-all-if-ambiguous on'
+ bind_cmds=$(grep '^bind .* # vyatta key binding$' $HOME/.bashrc)
+ eval $bind_cmds
+ ;;
+ esac
+ eval $nullglob_save
+}
+
+_vyatta_op_do_key_bindings
+
+test -f /etc/default/vyatta && \
+ source /etc/default/vyatta
+
+test ! -d "$vyatta_op_templates" && \
+ return 0
+
+case "$-" in
+ *i*)
+ declare -r _vyatta_op_last_comp_init='>>>>>>LASTCOMP<<<<<<'
+ ;;
+esac
+declare _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+declare _vyatta_op_node_path
+declare -a _vyatta_op_noncompletions _vyatta_op_completions
+declare -x -a _vyatta_pipe_noncompletions _vyatta_pipe_completions
+declare _vyatta_comptype
+declare -x -a reply
+declare -a _vyatta_operator_allowed
+
+if [[ "$VYATTA_USER_LEVEL_DIR" != "/opt/vyatta/etc/shell/level/admin" ]]; then
+ _vyatta_operator_allowed=( $(cat $VYATTA_USER_LEVEL_DIR/allowed-op) )
+fi
+
+declare -a functions
+functions=( /opt/vyatta/share/vyatta-op/functions/interpreter/* )
+
+for file in "${functions[@]}";do
+ source $file;
+done
+
+# $1: label
+# #2...: strings
+_vyatta_op_debug ()
+{
+ echo -ne \\n$1:
+ shift
+ for s ; do
+ echo -ne " \"$s\""
+ done
+}
+
+# this is needed to provide original "default completion" behavior.
+# see "vyatta-cfg" completion script for details.
+_vyatta_op_default_expand ()
+{
+ local wc=${#COMP_WORDS[@]}
+ if [[ "${COMP_WORDS[0]}" =~ "/" ]]; then
+ # if we are looking for a directory on the first completion then do directory completions
+ _filedir_xspec_vyos
+ elif (( wc < 2 )) ||
+ [[ $COMP_CWORD -eq 0 ]] ||
+ [[ $1 == $2 ]]; then
+ _vyatta_op_expand "$@"
+ else
+ # after the first word => cannot be vyatta command so use original default
+ _filedir_xspec_vyos
+ fi
+}
+
+# $1: label
+# $2...: help
+_vyatta_op_print_help ()
+{
+ local label=$1 help=$2
+ if [ ${#label} -eq 0 ] ; then
+ return
+ elif [ ${#help} -eq 0 ] ; then
+ echo -ne "\n $label"
+ elif [ ${#label} -lt 6 ] ; then
+ echo -ne "\n $label\t\t\t$help"
+ elif [ ${#label} -lt 14 ] ; then
+ echo -ne "\n $label\t\t$help"
+ elif [ ${#label} -lt 21 ] ; then
+ echo -ne "\n $label\t$help"
+ else
+ echo -ne "\n $label\n\t\t\t$help"
+ fi
+}
+
+# $1: $cur
+# $2...: possible completions
+_vyatta_op_help ()
+{
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -u nullglob
+ local cur=$1; shift
+ local ndef node_tag_help node_run help last_help
+
+ ndef=${_vyatta_op_node_path}/node.tag/node.def
+ [ -f $ndef ] && \
+ node_tag_help=$( _vyatta_op_get_node_def_field $ndef help )
+
+ ndef=${_vyatta_op_node_path}/node.def
+ [ -f $ndef ] && \
+ node_run=$( _vyatta_op_get_node_def_field $ndef run )
+
+ if [[ "$1" == "<nocomps>" ]]; then
+ eval "$restore_shopts"
+ return
+ fi
+ echo -en "\nPossible completions:"
+ if [ -z "$cur" -a -n "$node_run" ]; then
+ _vyatta_op_print_help '<Enter>' "Execute the current command"
+ fi
+ if [ $# -eq 0 ];then
+ _vyatta_op_print_help '<text>' "$node_tag_help"
+ eval "$restore_shopts"
+ return
+ fi
+ for comp ; do
+ if [[ "$comp" == "<Enter>" ]]; then
+ continue
+ fi
+ if [ -z "$comp" ] ; then
+ if [ "X$node_tag_help" == "X$last_help" ] ; then
+ help=""
+ else
+ last_help=$node_tag_help
+ help=$node_tag_help
+ fi
+ _vyatta_op_print_help '*' "$help"
+ elif [[ -z "$cur" || $comp == ${cur}* ]] ; then
+ ndef=${_vyatta_op_node_path}/$comp/node.def
+ if [ -f $ndef ] ; then
+ help=$( _vyatta_op_get_node_def_field $ndef help )
+ else
+ help=$node_tag_help
+ fi
+ if [ "X$help" == "X$last_help" ] ; then
+ help=""
+ else
+ last_help=$help
+ fi
+ _vyatta_op_print_help "$comp" "$help"
+ fi
+ done
+ eval "$restore_shopts"
+}
+
+_vyatta_op_set_node_path ()
+{
+ local node
+ _vyatta_op_node_path=$vyatta_op_templates
+ for (( i=0 ; i<COMP_CWORD ; i++ )) ; do
+ # expand the command so completion continues to work with short versions
+ if [[ "${COMP_WORDS[i]}" == "*" ]]; then
+ node="node.tag" # user defined wildcars are always tag nodes
+ else
+ node=$(_vyatta_op_conv_node_path $_vyatta_op_node_path ${COMP_WORDS[i]})
+ fi
+ if [ -f "${_vyatta_op_node_path}/$node/node.def" ] ; then
+ _vyatta_op_node_path+=/$node
+ elif [ -f ${_vyatta_op_node_path}/node.tag/node.def ] ; then
+ _vyatta_op_node_path+=/node.tag
+ else
+ return 1
+ fi
+ done
+}
+
+_vyatta_op_set_completions ()
+{
+ local -a allowed completions
+ local cur=$1
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ for ndef in ${_vyatta_op_node_path}/*/node.def ; do
+ if [[ $ndef == */node.tag/node.def ]] ; then
+ local acmd=$( _vyatta_op_get_node_def_field $ndef allowed )
+ shopt -u extglob nullglob
+ local -a a=($( eval "$acmd" ))
+ eval "$restore_shopts"
+
+ if [ ${#a[@]} -ne 0 ] ; then
+ allowed+=( "${a[@]}" )
+ else
+ allowed+=( "<text>" )
+ fi
+ else
+ local sdir=${ndef%/*}
+ allowed+=( ${sdir##*/} )
+ fi
+ done
+
+ # donot complete entries like <HOSTNAME> or <A.B.C.D>
+ _vyatta_op_noncompletions=( )
+ completions=( )
+
+ # make runable commands have a non-comp
+ ndef=${_vyatta_op_node_path}/node.def
+ [ -f $ndef ] && \
+ node_run=$( _vyatta_op_get_node_def_field $ndef run )
+ if [ -z "$cur" -a -n "$node_run" ]; then
+ _vyatta_op_noncompletions+=('<Enter>')
+ fi
+
+ for (( i=0 ; i<${#allowed[@]} ; i++ )) ; do
+ if [[ "${allowed[i]}" == \<*\> ]] ; then
+ _vyatta_op_noncompletions+=( "${allowed[i]}" )
+ else
+ if [[ "$VYATTA_USER_LEVEL_DIR" == "/opt/vyatta/etc/shell/level/admin" ]]; then
+ completions+=( ${allowed[i]} )
+ elif is_elem_of ${allowed[i]} _vyatta_operator_allowed; then
+ completions+=( ${allowed[i]} )
+ elif [[ $_vyatta_op_node_path == $vyatta_op_templates ]];then
+ continue
+ else
+ completions+=( ${allowed[i]} )
+ fi
+ fi
+ done
+
+ # Prefix filter the non empty completions
+ if [ -n "$cur" ]; then
+ _vyatta_op_completions=()
+ get_prefix_filtered_list "$cur" completions _vyatta_op_completions
+ _vyatta_op_completions=($( printf "%s\n" ${_vyatta_op_completions[@]} | sort -u ))
+ else
+ _vyatta_op_completions=($( printf "%s\n" ${completions[@]} | sort -u ))
+ fi
+ #shopt -s nullglob
+}
+
+_vyatta_op_comprely_needs_ambiguity ()
+{
+ local -a uniq
+
+ [ ${#COMPREPLY[@]} -eq 1 ] && return
+
+ uniq=( `printf "%s\n" ${COMPREPLY[@]} | cut -c1 | sort -u` )
+
+ [ ${#uniq[@]} -eq 1 ] && return
+ false
+}
+
+_vyatta_op_invalid_completion ()
+{
+ local tpath=$vyatta_op_templates
+ local -a args
+ local i=1
+ for arg in "${COMP_WORDS[@]}"; do
+ arg=( $(_vyatta_op_conv_node_path $tpath $arg) ) # expand the arguments
+ # output proper error message based on the above expansion
+ if [[ "${arg[1]}" == "ambiguous" ]]; then
+ echo -ne "\n\n Ambiguous command: ${args[@]} [$arg]\n"
+ local -a cmds=( $(compgen -d $tpath/$arg) )
+ _vyatta_op_node_path=$tpath
+ local comps=$(_vyatta_op_help $arg ${cmds[@]##*/})
+ echo -ne "$comps" | sed -e 's/^P/ P/'
+ break
+ elif [[ "${arg[1]}" == "invalid" ]]; then
+ echo -ne "\n\n Invalid command: ${args[@]} [$arg]"
+ break
+ fi
+
+ if [ -f "$tpath/$arg/node.def" ] ; then
+ tpath+=/$arg
+ elif [ -f $tpath/node.tag/node.def ] ; then
+ tpath+=/node.tag
+ else
+ echo -ne "\n\n Invalid command: ${args[@]} [$arg]" >&2
+ break
+ fi
+ args[$i]=$arg
+ let "i+=1"
+ if [ $[${#COMP_WORDS[@]}+1] -eq $i ];then
+ _vyatta_op_help "" \
+ "${_vyatta_op_noncompletions[@]}" \
+ "${_vyatta_op_completions[@]}" \
+ | ${VYATTA_PAGER:-cat}
+ fi
+ done
+}
+
+_vyatta_op_expand ()
+{
+ # We need nospace here and we have to append our own spaces
+ compopt -o nospace
+
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -s extglob nullglob
+ local cur=""
+ local _has_comptype=0
+ local current_prefix=$2
+ local current_word=$3
+ _vyatta_comptype=""
+
+ if (( ${#COMP_WORDS[@]} > 0 )); then
+ cur=${COMP_WORDS[COMP_CWORD]}
+ else
+ (( COMP_CWORD = ${#COMP_WORDS[@]} ))
+ fi
+
+ if _vyatta_pipe_completion "${COMP_WORDS[@]}"; then
+ if [ "${COMP_WORDS[*]}" == "$_vyatta_op_last_comp" ] ||
+ [ ${#_vyatta_pipe_completions[@]} -eq 0 ]; then
+ _vyatta_do_pipe_help
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ else
+ COMPREPLY=( "${_vyatta_pipe_completions[@]}" )
+ _vyatta_op_last_comp="${COMP_WORDS[*]}"
+ if [ ${#COMPREPLY[@]} -eq 1 ]; then
+ COMPREPLY=( "${COMPREPLY[0]} " )
+ fi
+ fi
+ eval "$restore_shopts"
+ return
+ fi
+
+ # this needs to be done on every completion even if it is the 'same' comp.
+ # The cursor can be at different places in the string.
+ # this will lead to unexpected cases if setting the node path isn't attempted
+ # each time.
+ if ! _vyatta_op_set_node_path ; then
+ echo -ne \\a
+ _vyatta_op_invalid_completion
+ COMPREPLY=( "" " " )
+ eval "$restore_shopts"
+ return 1
+ fi
+
+ if [ "${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}" != "$_vyatta_op_last_comp" ] ; then
+ _vyatta_set_comptype
+ case $_vyatta_comptype in
+ 'imagefiles')
+ _has_comptype=1
+ _vyatta_image_file_complete
+ ;;
+ *)
+ _has_comptype=0
+ if [[ -z "$current_word" ]]; then
+ _vyatta_op_set_completions $cur
+ else
+ _vyatta_op_set_completions $current_prefix
+ fi
+ ;;
+ esac
+ fi
+ if [[ $_has_comptype == 1 ]]; then
+ COMPREPLY=( "${_vyatta_op_completions[@]}" )
+ else
+ COMPREPLY=($( compgen -W "${_vyatta_op_completions[*]}" -- $current_prefix ))
+ fi
+
+ # if the last command line arg is empty and we have
+ # an empty completion option (meaning wild card),
+ # append a blank(s) to the completion array to force ambiguity
+ if [ -z "$current_prefix" -a -n "$current_word" ] ||
+ [[ "${COMPREPLY[0]}" =~ "$cur" ]]; then
+ for comp ; do
+ if [ -z "$comp" ] ; then
+ if [ ${#COMPREPLY[@]} -eq 0 ] ; then
+ COMPREPLY=( " " "" )
+ elif _vyatta_op_comprely_needs_ambiguity ; then
+ COMPREPLY+=( " " )
+ fi
+ fi
+ done
+ fi
+ # Set this environment to enable and disable debugging on the fly
+ if [[ $DBG_OP_COMPS -eq 1 ]]; then
+ echo -e "\nCurrent: '$cur'"
+ echo -e "Current word: '$current_word'"
+ echo -e "Current prefix: '$current_prefix'"
+ echo "Number of comps: ${#_vyatta_op_completions[*]}"
+ echo "Number of non-comps: ${#_vyatta_op_noncompletions[*]}"
+ echo "_vyatta_op_completions: '${_vyatta_op_completions[*]}'"
+ echo "COMPREPLY: '${COMPREPLY[@]}'"
+ echo "CWORD: $COMP_CWORD"
+ echo "Last comp: '$_vyatta_op_last_comp'"
+ echo -e "Current comp: '${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}'\n"
+ fi
+
+ # This is non obvious...
+ # To have completion continue to work when working with words that aren't the last word,
+ # we have to set nospace at the beginning of this script and then append the spaces here.
+ if [ ${#COMPREPLY[@]} -eq 1 ] &&
+ [[ $_has_comptype -ne 1 ]]; then
+ COMPREPLY=( "${COMPREPLY[0]} " )
+ fi
+ # if there are no completions then handle invalid commands
+ if [ ${#_vyatta_op_noncompletions[@]} -eq 0 ] &&
+ [ ${#_vyatta_op_completions[@]} -eq 0 ]; then
+ _vyatta_op_invalid_completion
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ elif [ ${#COMPREPLY[@]} -eq 0 ] &&
+ [ -n "$current_prefix" ]; then
+ _vyatta_op_invalid_completion
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ # Stop completions from getting stuck
+ elif [ ${#_vyatta_op_completions[@]} -eq 1 ] &&
+ [ -n "$cur" ] &&
+ [[ "${COMPREPLY[0]}" =~ "$cur" ]]; then
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ elif [ ${#_vyatta_op_completions[@]} -eq 1 ] &&
+ [ -n "$current_prefix" ] &&
+ [[ "${COMPREPLY[0]}" =~ "$current_prefix" ]]; then
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ # if there are no completions then always show the non-comps
+ elif [ "${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}" == "$_vyatta_op_last_comp" ] ||
+ [ ${#_vyatta_op_completions[@]} -eq 0 ] ||
+ [ -z "$cur" ]; then
+ _vyatta_op_help "$current_prefix" \
+ "${_vyatta_op_noncompletions[@]}" \
+ "${_vyatta_op_completions[@]}" \
+ | ${VYATTA_PAGER:-cat}
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ else
+ _vyatta_op_last_comp="${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}"
+ fi
+
+ eval "$restore_shopts"
+}
+
+# "pipe" functions
+count ()
+{
+ wc -l
+}
+
+match ()
+{
+ grep -E -e "$1"
+}
+
+no-match ()
+{
+ grep -E -v -e "$1"
+}
+
+no-more ()
+{
+ cat
+}
+
+strip-private ()
+{
+ ${vyos_libexec_dir}/strip-private.py
+}
+
+commands ()
+{
+ if [ "$_OFR_CONFIGURE" != "" ]; then
+ if $(cli-shell-api sessionChanged); then
+ echo "You have uncommited changes, please commit them before using the commands pipe"
+ else
+ vyos-config-to-commands
+ fi
+ else
+ echo "commands pipe is not supported in operational mode"
+ fi
+}
+
+json ()
+{
+ if [ "$_OFR_CONFIGURE" != "" ]; then
+ if $(cli-shell-api sessionChanged); then
+ echo "You have uncommited changes, please commit them before using the JSON pipe"
+ else
+ vyos-config-to-json
+ fi
+ else
+ echo "JSON pipe is not supported in operational mode"
+ fi
+}
+
+# pipe command help
+# $1: command
+_vyatta_pipe_help ()
+{
+ local help="No help text available"
+ case "$1" in
+ count) help="Count the number of lines in the output";;
+ match) help="Only output lines that match specified pattern";;
+ no-match) help="Only output lines that do not match specified pattern";;
+ more) help="Paginate the output";;
+ no-more) help="Do not paginate the output";;
+ strip-private) help="Remove private information from the config";;
+ commands) help="Convert config to set commands";;
+ json) help="Convert config to JSON";;
+ '<pattern>') help="Pattern for matching";;
+ esac
+ echo -n "$help"
+}
+
+_vyatta_do_pipe_help ()
+{
+ local help=''
+ if (( ${#_vyatta_pipe_completions[@]} + ${#_vyatta_pipe_noncompletions[@]}
+ == 0 )); then
+ return
+ fi
+ echo -en "\nPossible completions:"
+ for comp in "${_vyatta_pipe_completions[@]}" \
+ "${_vyatta_pipe_noncompletions[@]}"; do
+ _vyatta_op_print_help "$comp" "$(_vyatta_pipe_help "$comp")"
+ done
+}
+
+# pipe completion
+# $@: words
+_vyatta_pipe_completion ()
+{
+ local -a pipe_cmd=()
+ local -a all_cmds=( 'count' 'match' 'no-match' 'more' 'no-more' 'strip-private' 'commands' 'json' )
+ local found=0
+ _vyatta_pipe_completions=()
+ _vyatta_pipe_noncompletions=()
+
+ for word in "$@"; do
+ if [[ "$found" == "1" || "$word" == "|" ]]; then
+ pipe_cmd+=( "$word" )
+ found=1
+ fi
+ done
+ if (( found == 0 )); then
+ return 1
+ fi
+ if (( ${#pipe_cmd[@]} == 1 )); then
+ # "|" only
+ _vyatta_pipe_completions=( "${all_cmds[@]}" )
+ return 0
+ fi
+ if (( ${#pipe_cmd[@]} == 2 )); then
+ # "|<space, chars, or space+chars>"
+ _vyatta_pipe_completions=($(compgen -W "${all_cmds[*]}" -- ${pipe_cmd[1]}))
+ return 0
+ fi
+ if (( ${#pipe_cmd[@]} == 3 )); then
+ # "|<chars or space+chars><space or space+chars>"
+ case "${pipe_cmd[1]}" in
+ match|no-match) _vyatta_pipe_noncompletions=( '<pattern>' );;
+ esac
+ return 0
+ fi
+ return 0
+}
+
+# comptype
+_vyatta_set_comptype ()
+{
+ local comptype
+ unset _vyatta_comptype
+ for ndef in ${_vyatta_op_node_path}/*/node.def ; do
+ if [[ $ndef == */node.tag/node.def ]] ; then
+ local comptype=$( _vyatta_op_get_node_def_field $ndef comptype )
+ if [[ $comptype == "imagefiles" ]] ; then
+ _vyatta_comptype=$comptype
+ return 0
+ else
+ _vyatta_comptype=""
+ return 1
+ fi
+ else
+ _vyatta_comptype=""
+ return 1
+ fi
+ done
+}
+
+_filedir_xspec_vyos()
+{
+ local cur prev words cword
+ _init_completion || return
+
+ _tilde "$cur" || return 0
+
+ local IFS=$'\n' xspec=${_xspec[${1##*/}]} tmp
+ local -a toks
+
+ toks=( $(
+ compgen -d -- "$(quote_readline "$cur")" | {
+ while read -r tmp; do
+ printf '%s\n' $tmp
+ done
+ }
+ ))
+
+ # Munge xspec to contain uppercase version too
+ # http://thread.gmane.org/gmane.comp.shells.bash.bugs/15294/focus=15306
+ eval xspec="${xspec}"
+ local matchop=!
+ if [[ $xspec == !* ]]; then
+ xspec=${xspec#!}
+ matchop=@
+ fi
+ xspec="$matchop($xspec|${xspec^^})"
+
+ toks+=( $(
+ eval compgen -f -X "!$xspec" -- "\$(quote_readline "\$cur")" | {
+ while read -r tmp; do
+ [[ -n $tmp ]] && printf '%s\n' $tmp
+ done
+ }
+ ))
+
+ if [[ ${#toks[@]} -ne 0 ]]; then
+ compopt -o filenames
+ COMPREPLY=( "${toks[@]}" )
+ fi
+}
+
+nullglob_save=$( shopt -p nullglob )
+shopt -s nullglob
+for f in ${vyatta_datadir}/vyatta-op/functions/allowed/* ; do
+ source $f
+done
+eval $nullglob_save
+unset nullglob_save
+
+# don't initialize if we are in configure mode
+if [ "$_OFR_CONFIGURE" == "ok" ]; then
+ return 0
+fi
+
+if [[ "$VYATTA_USER_LEVEL_DIR" != "/opt/vyatta/etc/shell/level/admin" ]]; then
+ vyatta_unpriv_init $@
+else
+ _vyatta_op_init $@
+fi
+
+### Local Variables:
+### mode: shell-script
+### End:
diff --git a/src/etc/default/vyatta b/src/etc/default/vyatta
new file mode 100644
index 000000000..e5fa3bb30
--- /dev/null
+++ b/src/etc/default/vyatta
@@ -0,0 +1,217 @@
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+
+# declare configured Vyatta shell environment variables
+
+# first set vars per args of the "source /etc/default/vyatta VAR=FOO"
+_vyatta_extglob=$(shopt -p extglob)
+shopt -s extglob
+for arg ; do
+ [[ $arg == *=* ]] && \
+ eval declare -x $arg
+done
+eval $_vyatta_extglob
+unset _vyatta_extglob
+
+{
+ # These declarations must go within braces in order to be able to silence
+ # readonly variable errors.
+
+ for var in prefix exec_prefix datarootdir ; do
+ eval test -n \"\$$var\" \&\& _vyatta_save_$var=\$$var
+ done
+
+ prefix=/opt/vyatta
+ exec_prefix=${prefix}
+ datarootdir=${prefix}/share
+
+ if test -z "$vyatta_prefix" ; then
+ if test -n "/opt/vyatta" ; then
+ declare -x -r vyatta_prefix=/opt/vyatta
+ declare -x -r vyos_prefix=/opt/vyatta
+ else
+ declare -x -r vyatta_prefix=/opt/vyatta
+ declare -x -r vyos_prefix=/opt/vyatta
+ fi
+ fi
+ if test -z "$vyatta_exec_prefix" ; then
+ if test -n "${prefix}" ; then
+ declare -x -r vyatta_prefix=${prefix}
+ declare -x -r vyos_prefix=${prefix}
+ else
+ declare -x -r vyatta_prefix=$vyatta_prefix
+ declare -x -r vyos_prefix=$vyatta_prefix
+ fi
+ fi
+ if test -z "$vyatta_datarootdir" ; then
+ if test -n "${prefix}/share" ; then
+ declare -x -r vyatta_datarootdir=${prefix}/share
+ declare -x -r vyos_datarootdir=${prefix}/share
+ else
+ declare -x -r vyatta_datarootdir=$vyatta_prefix/share
+ declare -x -r vyos_datarootdir=$vyatta_prefix/share
+ fi
+ fi
+ if test -z "$vyatta_bindir" ; then
+ if test -n "${exec_prefix}/bin" ; then
+ declare -x -r vyatta_bindir=${exec_prefix}/bin
+ else
+ declare -x -r vyatta_bindir=$vyatta_exec_prefix/bin
+ fi
+ fi
+ if test -z "$vyatta_sbindir" ; then
+ if test -n "${exec_prefix}/sbin" ; then
+ declare -x -r vyatta_sbindir=${exec_prefix}/sbin
+ else
+ declare -x -r vyatta_sbindir=$vyatta_exec_prefix/sbin
+ fi
+ fi
+ if test -z "$vyatta_libdir" ; then
+ if test -n "${exec_prefix}/lib" ; then
+ declare -x -r vyatta_libdir=${exec_prefix}/lib
+ declare -x -r vyos_libdir=${exec_prefix}/lib
+ else
+ declare -x -r vyatta_libdir=$vyatta_exec_prefix/lib
+ declare -x -r vyos_libdir=$vyatta_exec_prefix/lib
+ fi
+ fi
+ if test -z "$vyatta_libexecdir" ; then
+ if test -n "${exec_prefix}/libexec" ; then
+ declare -x -r vyatta_libexecdir=${exec_prefix}/libexec
+ else
+ declare -x -r vyatta_libexecdir=$vyatta_exec_prefix/libexec
+ fi
+ fi
+ if test -z "$vyatta_datadir" ; then
+ if test -n "${datarootdir}" ; then
+ declare -x -r vyatta_datadir=${datarootdir}
+ declare -x -r vyos_datadir=${datarootdir}
+ else
+ declare -x -r vyatta_datadir=$vyatta_datarootdir
+ declare -x -r vyos_datadir=$vyatta_datarootdir
+ fi
+ fi
+ if test -z "$vyatta_htmldir" ; then
+ if test -n "${docdir}" ; then
+ declare -x -r vyatta_htmldir=${docdir}
+ else
+ declare -x -r vyatta_htmldir=$vyatta_datarootdir/html
+ fi
+ fi
+ if test -z "$vyatta_infodir" ; then
+ if test -n "${prefix}/share/info" ; then
+ declare -x -r vyatta_infodir=${prefix}/share/info
+ else
+ declare -x -r vyatta_infodir=$vyatta_datarootdir/info
+ fi
+ fi
+ if test -z "$vyatta_mandir" ; then
+ if test -n "${prefix}/share/man" ; then
+ declare -x -r vyatta_htmldir=${prefix}/share/man
+ else
+ declare -x -r vyatta_htmldir=$vyatta_datarootdir/man
+ fi
+ fi
+ if test -z "$vyatta_localedir" ; then
+ if test -n "${datarootdir}/locale" ; then
+ declare -x -r vyatta_localedir=${datarootdir}/locale
+ else
+ declare -x -r vyatta_localedir=$vyatta_datarootdir/locale
+ fi
+ fi
+ if test -z "$vyatta_localstatedir" ; then
+ if test -n "${prefix}/var" ; then
+ declare -x -r vyatta_localstatedir=${prefix}/var
+ else
+ declare -x -r vyatta_localstatedir=$vyatta_prefix/var
+ fi
+ fi
+ if test -z "$vyatta_sharedstatedir" ; then
+ if test -n "${prefix}/com" ; then
+ declare -x -r vyatta_sharedstatedir=${prefix}/com
+ else
+ declare -x -r vyatta_sharedstatedir=$vyatta_prefix/com
+ fi
+ fi
+ if test -z "$vyatta_sysconfdir" ; then
+ if test -n "${prefix}/etc" ; then
+ declare -x -r vyatta_sysconfdir=${prefix}/etc
+ else
+ declare -x -r vyatta_sysconfdir=$vyatta_prefix/etc
+ fi
+ fi
+ if test -z "$vyatta_op_templates" ; then
+ declare -x -r vyatta_op_templates=$vyatta_datadir/vyatta-op/templates
+ declare -x -r vyos_op_templates=$vyatta_datadir/vyatta-op/templates
+ fi
+ if test -z "$vyatta_cfg_templates" ; then
+ declare -x -r vyatta_cfg_templates=$vyatta_datadir/vyatta-cfg/templates
+ declare -x -r vyos_cfg_templates=$vyatta_datadir/vyatta-cfg/templates
+ fi
+ if test -z "$vyatta_configdir" ; then
+ declare -x -r vyatta_configdir=$vyatta_prefix/config
+ declare -x -r vyos_configdir=$vyatta_prefix/config
+ fi
+
+ for var in prefix exec_prefix datarootdir ; do
+ eval test -n \"\$_vyatta_save_$var\" \&\& $var=\$_vyatta_save_$var
+ done
+
+ # It's not like we do, or should support installing VyOS at a different prefix
+ declare -x -r vyos_libexec_dir=/usr/libexec/vyos
+ declare -x -r vyos_bin_dir=/usr/bin
+ declare -x -r vyos_sbin_dir=/usr/sbin
+ declare -x -r vyos_share_dir=/usr/share
+
+ if test -z "$vyos_conf_scripts_dir" ; then
+ declare -x -r vyos_conf_scripts_dir=$vyos_libexec_dir/conf_mode
+ fi
+ if test -z "$vyos_op_scripts_dir" ; then
+ declare -x -r vyos_op_scripts_dir=$vyos_libexec_dir/op_mode
+ fi
+ if test -z "$vyos_completion_dir" ; then
+ declare -x -r vyos_completion_dir=$vyos_libexec_dir/completion
+ fi
+ if test -z "$vyos_validators_dir" ; then
+ declare -x -r vyos_validators_dir=$vyos_libexec_dir/validators
+ fi
+ if test -z "$vyos_data_dir" ; then
+ declare -x -r vyos_data_dir=$vyos_share_dir/vyos
+ fi
+ if test -z "$vyos_persistence_dir" ; then
+ UNION_NAME=$(cat /proc/cmdline | sed -e s+^.*vyos-union=++ | sed -e 's/ .*$//')
+ declare -x -r vyos_persistence_dir="/usr/lib/live/mount/persistence/${UNION_NAME}"
+ fi
+ if test -z "$vyos_rootfs_dir" ; then
+ ROOTFS=$(mount -t squashfs | grep loop0 | cut -d' ' -f3)
+ declare -x -r vyos_rootfs_dir="${ROOTFS}"
+ fi
+ if test -z "$VRF" ; then
+ VRF=$(ip vrf identify)
+ [ -n "$VRF" ] && declare -x -r VRF="${VRF}"
+ fi
+ if test -z "$NETNS" ; then
+ NETNS=$(ip netns identify)
+ [ -n "$NETNS" ] && declare -x -r NETNS="${NETNS}"
+ fi
+
+} 2>/dev/null || :
+
+[ -r /etc/default/vyatta-cfg ] && source /etc/default/vyatta-cfg
+
+[ -r /etc/default/vyatta-local-env ] && source /etc/default/vyatta-local-env
+
+### Local Variables:
+### mode: shell-script
+### End:
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
index 518e204f9..f5de182c6 100755
--- a/src/helpers/vyos_net_name
+++ b/src/helpers/vyos_net_name
@@ -18,42 +18,35 @@ import os
import re
import time
import logging
+import logging.handlers
import tempfile
-import threading
+from pathlib import Path
from sys import argv
from vyos.configtree import ConfigTree
from vyos.defaults import directories
from vyos.utils.process import cmd
from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.locking import Lock
from vyos.migrate import ConfigMigrate
+# Define variables
vyos_udev_dir = directories['vyos_udev_dir']
-vyos_log_dir = '/run/udev/log'
-vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name')
-
config_path = '/opt/vyatta/etc/config/config.boot'
-lock = threading.Lock()
-
-try:
- os.mkdir(vyos_log_dir)
-except FileExistsError:
- pass
-
-logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG)
def is_available(intfs: dict, intf_name: str) -> bool:
- """ Check if interface name is already assigned
- """
+ """Check if interface name is already assigned"""
if intf_name in list(intfs.values()):
return False
return True
+
def find_available(intfs: dict, prefix: str) -> str:
- """ Find lowest indexed iterface name that is not assigned
- """
- index_list = [int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x]
+ """Find lowest indexed iterface name that is not assigned"""
+ index_list = [
+ int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x
+ ]
index_list.sort()
# find 'holes' in list, if any
missing = sorted(set(range(index_list[0], index_list[-1])) - set(index_list))
@@ -62,21 +55,22 @@ def find_available(intfs: dict, prefix: str) -> str:
return f'{prefix}{len(index_list)}'
+
def mod_ifname(ifname: str) -> str:
- """ Check interface with names eX and return ifname on the next format eth{ifindex} - 2
- """
- if re.match("^e[0-9]+$", ifname):
- intf = ifname.split("e")
+ """Check interface with names eX and return ifname on the next format eth{ifindex} - 2"""
+ if re.match('^e[0-9]+$', ifname):
+ intf = ifname.split('e')
if intf[1]:
if int(intf[1]) >= 2:
- return "eth" + str(int(intf[1]) - 2)
+ return 'eth' + str(int(intf[1]) - 2)
else:
- return "eth" + str(intf[1])
+ return 'eth' + str(intf[1])
return ifname
+
def get_biosdevname(ifname: str) -> str:
- """ Use legacy vyatta-biosdevname to query for name
+ """Use legacy vyatta-biosdevname to query for name
This is carried over for compatability only, and will likely be dropped
going forward.
@@ -95,11 +89,12 @@ def get_biosdevname(ifname: str) -> str:
try:
biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}')
except Exception as e:
- logging.error(f'biosdevname error: {e}')
+ logger.error(f'biosdevname error: {e}')
biosname = ''
return intf if biosname == '' else biosname
+
def leave_rescan_hint(intf_name: str, hwid: str):
"""Write interface information reported by udev
@@ -112,18 +107,18 @@ def leave_rescan_hint(intf_name: str, hwid: str):
except FileExistsError:
pass
except Exception as e:
- logging.critical(f"Error creating rescan hint directory: {e}")
+ logger.critical(f'Error creating rescan hint directory: {e}')
exit(1)
try:
with open(os.path.join(vyos_udev_dir, intf_name), 'w') as f:
f.write(hwid)
except OSError as e:
- logging.critical(f"OSError {e}")
+ logger.critical(f'OSError {e}')
+
def get_configfile_interfaces() -> dict:
- """Read existing interfaces from config file
- """
+ """Read existing interfaces from config file"""
interfaces: dict = {}
if not os.path.isfile(config_path):
@@ -134,14 +129,14 @@ def get_configfile_interfaces() -> dict:
with open(config_path) as f:
config_file = f.read()
except OSError as e:
- logging.critical(f"OSError {e}")
+ logger.critical(f'OSError {e}')
exit(1)
try:
config = ConfigTree(config_file)
except Exception:
try:
- logging.debug(f"updating component version string syntax")
+ logger.debug('updating component version string syntax')
# this will update the component version string syntax,
# required for updates 1.2 --> 1.3/1.4
with tempfile.NamedTemporaryFile() as fp:
@@ -157,7 +152,8 @@ def get_configfile_interfaces() -> dict:
config = ConfigTree(config_file)
except Exception as e:
- logging.critical(f"ConfigTree error: {e}")
+ logger.critical(f'ConfigTree error: {e}')
+ exit(1)
base = ['interfaces', 'ethernet']
if config.exists(base):
@@ -165,11 +161,13 @@ def get_configfile_interfaces() -> dict:
for intf in eth_intfs:
path = base + [intf, 'hw-id']
if not config.exists(path):
- logging.warning(f"no 'hw-id' entry for {intf}")
+ logger.warning(f"no 'hw-id' entry for {intf}")
continue
hwid = config.return_value(path)
if hwid in list(interfaces):
- logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+ logger.warning(
+ f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+ )
continue
interfaces[hwid] = intf
@@ -179,21 +177,23 @@ def get_configfile_interfaces() -> dict:
for intf in wlan_intfs:
path = base + [intf, 'hw-id']
if not config.exists(path):
- logging.warning(f"no 'hw-id' entry for {intf}")
+ logger.warning(f"no 'hw-id' entry for {intf}")
continue
hwid = config.return_value(path)
if hwid in list(interfaces):
- logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+ logger.warning(
+ f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+ )
continue
interfaces[hwid] = intf
- logging.debug(f"config file entries: {interfaces}")
+ logger.debug(f'config file entries: {interfaces}')
return interfaces
+
def add_assigned_interfaces(intfs: dict):
- """Add interfaces found by previous invocation of udev rule
- """
+ """Add interfaces found by previous invocation of udev rule"""
if not os.path.isdir(vyos_udev_dir):
return
@@ -203,55 +203,74 @@ def add_assigned_interfaces(intfs: dict):
with open(path) as f:
hwid = f.read().rstrip()
except OSError as e:
- logging.error(f"OSError {e}")
+ logger.error(f'OSError {e}')
continue
intfs[hwid] = intf
+
def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str:
- """Called on boot by vyos-router: 'coldplug' in vyatta_net_name
- """
- logging.info(f"lookup {intf_name}, {hwid}")
+ """Called on boot by vyos-router: 'coldplug' in vyatta_net_name"""
+ logger.info(f'lookup {intf_name}, {hwid}')
interfaces = get_configfile_interfaces()
- logging.debug(f"config file interfaces are {interfaces}")
+ logger.debug(f'config file interfaces are {interfaces}')
if hwid in list(interfaces):
- logging.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
+ logger.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
return interfaces[hwid]
add_assigned_interfaces(interfaces)
- logging.debug(f"adding assigned interfaces: {interfaces}")
+ logger.debug(f'adding assigned interfaces: {interfaces}')
if predefined:
newname = predefined
- logging.info(f"predefined interface name for '{intf_name}' is '{newname}'")
+ logger.info(f"predefined interface name for '{intf_name}' is '{newname}'")
else:
newname = get_biosdevname(intf_name)
- logging.info(f"biosdevname returned '{newname}' for '{intf_name}'")
+ logger.info(f"biosdevname returned '{newname}' for '{intf_name}'")
if not is_available(interfaces, newname):
prefix = re.sub(r'\d+$', '', newname)
newname = find_available(interfaces, prefix)
- logging.info(f"new name for '{intf_name}' is '{newname}'")
+ logger.info(f"new name for '{intf_name}' is '{newname}'")
leave_rescan_hint(newname, hwid)
return newname
+
def hotplug_event():
# Not yet implemented, since interface-rescan will only be run on boot.
pass
-if len(argv) > 3:
- predef_name = argv[3]
-else:
- predef_name = ''
-
-lock.acquire()
-if not boot_configuration_complete():
- res = on_boot_event(argv[1], argv[2], predefined=predef_name)
- logging.debug(f"on boot, returned name is {res}")
- print(res)
-else:
- logging.debug("boot configuration complete")
-lock.release()
+
+if __name__ == '__main__':
+ # Set up logging to syslog
+ syslog_handler = logging.handlers.SysLogHandler(address='/dev/log')
+ formatter = logging.Formatter(f'{Path(__file__).name}: %(message)s')
+ syslog_handler.setFormatter(formatter)
+
+ logger = logging.getLogger()
+ logger.addHandler(syslog_handler)
+ logger.setLevel(logging.DEBUG)
+
+ logger.debug(f'Started with arguments: {argv}')
+
+ if len(argv) > 3:
+ predef_name = argv[3]
+ else:
+ predef_name = ''
+
+ lock = Lock('vyos_net_name')
+ # Wait 60 seconds for other running scripts to finish
+ lock.acquire(60)
+
+ if not boot_configuration_complete():
+ res = on_boot_event(argv[1], argv[2], predefined=predef_name)
+ logger.debug(f'on boot, returned name is {res}')
+ print(res)
+ else:
+ logger.debug('boot configuration complete')
+
+ lock.release()
+ logger.debug('Finished')
diff --git a/src/migration-scripts/firewall/16-to-17 b/src/migration-scripts/firewall/16-to-17
new file mode 100755
index 000000000..ad0706f04
--- /dev/null
+++ b/src/migration-scripts/firewall/16-to-17
@@ -0,0 +1,60 @@
+# Copyright (C) 2024 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/>.
+
+#
+# T4694: Adding rt ipsec exists/missing match to firewall configs.
+# This involves a syntax change for IPsec matches, reflecting that different
+# nftables expressions are required depending on whether we're matching a
+# decrypted packet or a packet that will be encrypted - it's directional.
+# The old rules only matched decrypted packets, those matches are now *-in:
+ # from: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec|match-none
+ # to: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec-in|match-none-in
+#
+# The <chainspec> positions this match allowed were:
+# name (any custom chains), forward filter, input filter, prerouting raw.
+# There are positions where it was possible to set, but it would never commit
+# (nftables rejects 'meta ipsec' in output hooks), they are not considered here.
+#
+
+from vyos.configtree import ConfigTree
+
+firewall_base = ['firewall']
+
+def migrate_chain(config: ConfigTree, path: list[str]) -> None:
+ if not config.exists(path + ['rule']):
+ return
+
+ for rule_num in config.list_nodes(path + ['rule']):
+ tmp_path = path + ['rule', rule_num, 'ipsec']
+ if config.exists(tmp_path + ['match-ipsec']):
+ config.delete(tmp_path + ['match-ipsec'])
+ config.set(tmp_path + ['match-ipsec-in'])
+ elif config.exists(tmp_path + ['match-none']):
+ config.delete(tmp_path + ['match-none'])
+ config.set(tmp_path + ['match-none-in'])
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(firewall_base):
+ # Nothing to do
+ return
+
+ for family in ['ipv4', 'ipv6']:
+ tmp_path = firewall_base + [family, 'name']
+ if config.exists(tmp_path):
+ for custom_fwname in config.list_nodes(tmp_path):
+ migrate_chain(config, tmp_path + [custom_fwname])
+
+ for base_hook in [['forward', 'filter'], ['input', 'filter'], ['prerouting', 'raw']]:
+ tmp_path = firewall_base + [family] + base_hook
+ migrate_chain(config, tmp_path)
diff --git a/src/migration-scripts/firewall/7-to-8 b/src/migration-scripts/firewall/7-to-8
index f46994ce2..b8bcc52cc 100644
--- a/src/migration-scripts/firewall/7-to-8
+++ b/src/migration-scripts/firewall/7-to-8
@@ -71,5 +71,11 @@ def migrate(config: ConfigTree) -> None:
config.set_tag(['firewall', 'zone'])
for zone in config.list_nodes(zone_base + ['zone']):
+ if 'interface' in config.list_nodes(zone_base + ['zone', zone]):
+ for iface in config.return_values(zone_base + ['zone', zone, 'interface']):
+ if '+' in iface:
+ config.delete_value(zone_base + ['zone', zone, 'interface'], value=iface)
+ iface = iface.replace('+', '*')
+ config.set(zone_base + ['zone', zone, 'interface'], value=iface, replace=False)
config.copy(zone_base + ['zone', zone], ['firewall', 'zone', zone])
- config.delete(zone_base)
+ config.delete(zone_base) \ No newline at end of file
diff --git a/src/migration-scripts/nat/6-to-7 b/src/migration-scripts/nat/6-to-7
index 81b413e36..e9b90fc98 100644
--- a/src/migration-scripts/nat/6-to-7
+++ b/src/migration-scripts/nat/6-to-7
@@ -47,6 +47,8 @@ def migrate(config: ConfigTree) -> None:
tmp = config.return_value(base + [iface, 'interface-name'])
if tmp != 'any':
config.delete(base + [iface, 'interface-name'])
+ if '+' in tmp:
+ tmp = tmp.replace('+', '*')
config.set(base + [iface, 'name'], value=tmp)
else:
config.delete(base + [iface])
diff --git a/src/migration-scripts/openvpn/1-to-2 b/src/migration-scripts/openvpn/1-to-2
index b7b7d4c77..2baa7302c 100644
--- a/src/migration-scripts/openvpn/1-to-2
+++ b/src/migration-scripts/openvpn/1-to-2
@@ -20,12 +20,8 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
- for i in ovpn_intfs:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
# Remove 'encryption cipher' and add this value to 'encryption ncp-ciphers'
# for server and client mode.
# Site-to-site mode still can use --cipher option
diff --git a/src/migration-scripts/openvpn/2-to-3 b/src/migration-scripts/openvpn/2-to-3
new file mode 100644
index 000000000..4e6b3c8b7
--- /dev/null
+++ b/src/migration-scripts/openvpn/2-to-3
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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/>.
+#
+# Adds an explicit old default for 'server topology'
+# to keep old configs working as before even though the default has changed.
+
+from vyos.configtree import ConfigTree
+
+def migrate(config: ConfigTree) -> None:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
+ mode = config.return_value(['interfaces', 'openvpn', i, 'mode'])
+ if mode != 'server':
+ # If it's a client or a site-to-site OpenVPN interface,
+ # the topology setting is not applicable
+ # and will cause commit errors on load,
+ # so we must not change such interfaces.
+ continue
+ else:
+ # The default OpenVPN server topology was changed from net30 to subnet
+ # because net30 is deprecated and causes problems with Windows clients.
+ # We add 'net30' to old configs if topology is not set there
+ # to ensure that if anyone relies on net30, their configs work as before.
+ topology_path = ['interfaces', 'openvpn', i, 'server', 'topology']
+ if not config.exists(topology_path):
+ config.set(topology_path, value='net30', replace=False)
diff --git a/src/migration-scripts/openvpn/3-to-4 b/src/migration-scripts/openvpn/3-to-4
new file mode 100644
index 000000000..0529491c1
--- /dev/null
+++ b/src/migration-scripts/openvpn/3-to-4
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 <http://www.gnu.org/licenses/>.
+# Renames ncp-ciphers option to data-ciphers
+
+from vyos.configtree import ConfigTree
+
+def migrate(config: ConfigTree) -> None:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
+ #Rename 'encryption ncp-ciphers' with 'encryption data-ciphers'
+ ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers']
+ if config.exists(ncp_cipher_path):
+ config.rename(ncp_cipher_path, 'data-ciphers')
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
index d04f1541f..e80b1c21d 100755
--- a/src/op_mode/bridge.py
+++ b/src/op_mode/bridge.py
@@ -70,7 +70,7 @@ def _get_raw_data_fdb(bridge):
# From iproute2 fdb.c, fdb_show() will only exit(-1) in case of
# non-existent bridge device; raise error.
if code == 255:
- raise vyos.opmode.UnconfiguredSubsystem(f"no such bridge device {bridge}")
+ raise vyos.opmode.UnconfiguredObject(f"bridge {bridge} does not exist in the system")
data_dict = json.loads(json_data)
return data_dict
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index 373f9e953..8903f916a 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -95,17 +95,21 @@ def disconnect(interface):
def main():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
- group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store")
- group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store")
+ group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store_true")
+ group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store_true")
+ parser.add_argument("--interface", help="Interface name", action="store", required=True)
args = parser.parse_args()
- if args.connect:
- if commit_in_progress():
- print('Cannot connect while a commit is in progress')
- exit(1)
- connect(args.connect)
- elif args.disconnect:
- disconnect(args.disconnect)
+ if args.connect or args.disconnect:
+ if args.disconnect:
+ disconnect(args.interface)
+
+ if args.connect:
+ if commit_in_progress():
+ print('Cannot connect while a commit is in progress')
+ exit(1)
+ connect(args.interface)
+
else:
parser.print_help()
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 6f57f22a5..e5455c8af 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -332,7 +332,7 @@ def _verify_client(func):
# Check if config does not exist
if not config.exists(f'interfaces {interface_path} address dhcp{v}'):
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ raise vyos.opmode.UnconfiguredObject(unconf_message)
return func(*args, **kwargs)
return _wrapper
diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py
index 14ffdca9f..e7afc4caa 100755
--- a/src/op_mode/interfaces.py
+++ b/src/op_mode/interfaces.py
@@ -445,12 +445,24 @@ def _format_show_counters(data: list):
print (output)
return output
+
+def _show_raw(data: list, intf_name: str):
+ if intf_name is not None and len(data) <= 1:
+ try:
+ return data[0]
+ except IndexError:
+ raise vyos.opmode.UnconfiguredObject(
+ f"Interface {intf_name} does not exist")
+ else:
+ return data
+
+
def show(raw: bool, intf_name: typing.Optional[str],
intf_type: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_raw_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_data(data)
def show_summary(raw: bool, intf_name: typing.Optional[str],
@@ -458,7 +470,7 @@ def show_summary(raw: bool, intf_name: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_summary_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_summary(data)
def show_summary_extended(raw: bool, intf_name: typing.Optional[str],
@@ -466,7 +478,7 @@ def show_summary_extended(raw: bool, intf_name: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_summary_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_summary_extended(data)
def show_counters(raw: bool, intf_name: typing.Optional[str],
@@ -474,7 +486,7 @@ def show_counters(raw: bool, intf_name: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_counter_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_counters(data)
def clear_counters(intf_name: typing.Optional[str],
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 44d41219e..c8f5072da 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright (C) 2022-2024 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
@@ -13,6 +13,7 @@
#
# 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 pprint
import re
import sys
import typing
@@ -25,6 +26,7 @@ from vyos.utils.convert import convert_data
from vyos.utils.convert import seconds_to_human
from vyos.utils.process import cmd
from vyos.configquery import ConfigTreeQuery
+from vyos.base import Warning
import vyos.opmode
import vyos.ipsec
@@ -43,7 +45,7 @@ def _get_raw_data_sas():
get_sas = vyos.ipsec.get_vici_sas()
sas = convert_data(get_sas)
return sas
- except (vyos.ipsec.ViciInitiateError) as err:
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
@@ -56,11 +58,10 @@ def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str:
:return: formatted string
:rtype: str
"""
- output = '';
+ 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"]}')
+ swanctl_output: str = cmd(f'sudo swanctl -l --ike-id {sa["uniqueid"]}')
output = f'{output}{swanctl_output}\n\n'
return output
@@ -72,7 +73,9 @@ def _get_formatted_output_sas(sas):
# create an item for each child-sa
for child_sa in parent_sa.get('child-sas', {}).values():
# prepare a list for output data
- sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A'
+ sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = (
+ sa_out_packets
+ ) = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A'
# collect raw data
sa_name = child_sa.get('name')
@@ -104,10 +107,8 @@ def _get_formatted_output_sas(sas):
bytes_out = filesize.size(int(sa_bytes_out))
sa_out_bytes = f'{bytes_in}/{bytes_out}'
if sa_packets_in and sa_packets_out:
- packets_in = filesize.size(int(sa_packets_in),
- system=filesize.si)
- packets_out = filesize.size(int(sa_packets_out),
- system=filesize.si)
+ packets_in = filesize.size(int(sa_packets_in), system=filesize.si)
+ packets_out = filesize.size(int(sa_packets_out), system=filesize.si)
packets_str = f'{packets_in}/{packets_out}'
sa_out_packets = re.sub(r'B', r'', packets_str)
if sa_remote_addr:
@@ -119,7 +120,9 @@ def _get_formatted_output_sas(sas):
sa_out_proposal = sa_proposal_encr_alg
if sa_proposal_encr_keysize:
sa_proposal_encr_keysize_str = sa_proposal_encr_keysize
- sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+ sa_out_proposal = (
+ f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+ )
if sa_proposal_integ_alg:
sa_proposal_integ_alg_str = sa_proposal_integ_alg
sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}'
@@ -128,15 +131,28 @@ def _get_formatted_output_sas(sas):
sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}'
# add a new item to output data
- sa_data.append([
- sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes,
- sa_out_packets, sa_out_remote_addr, sa_out_remote_id,
- sa_out_proposal
- ])
+ sa_data.append(
+ [
+ sa_out_name,
+ sa_out_state,
+ sa_out_uptime,
+ sa_out_bytes,
+ sa_out_packets,
+ sa_out_remote_addr,
+ sa_out_remote_id,
+ sa_out_proposal,
+ ]
+ )
headers = [
- "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out",
- "Remote address", "Remote ID", "Proposal"
+ 'Connection',
+ 'State',
+ 'Uptime',
+ 'Bytes In/Out',
+ 'Packets In/Out',
+ 'Remote address',
+ 'Remote ID',
+ 'Proposal',
]
sa_data = sorted(sa_data, key=_alphanum_key)
output = tabulate(sa_data, headers)
@@ -145,14 +161,16 @@ def _get_formatted_output_sas(sas):
# Connections block
+
def _get_convert_data_connections():
try:
get_connections = vyos.ipsec.get_vici_connections()
connections = convert_data(get_connections)
return connections
- except (vyos.ipsec.ViciInitiateError) as err:
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
+
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
@@ -184,7 +202,7 @@ def _get_parent_sa_proposal(connection_name: str, data: list) -> dict:
'mode': mode,
'key_size': encr_keysize,
'hash': integ_alg,
- 'dh': dh_group
+ 'dh': dh_group,
}
return proposal
return {}
@@ -213,8 +231,7 @@ def _get_parent_sa_state(connection_name: str, data: list) -> str:
return ike_state
-def _get_child_sa_state(connection_name: str, tunnel_name: str,
- data: list) -> str:
+def _get_child_sa_state(connection_name: str, tunnel_name: str, data: list) -> str:
"""Get child SA state by connection and tunnel name
Args:
@@ -236,14 +253,12 @@ def _get_child_sa_state(connection_name: str, tunnel_name: str,
# 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
+ v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name
]
return 'up' if 'INSTALLED' in child_sa_states else child_sa
-def _get_child_sa_info(connection_name: str, tunnel_name: str,
- data: list) -> dict:
+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:
@@ -264,8 +279,9 @@ def _get_child_sa_info(connection_name: str, tunnel_name: str,
# {'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'
+ 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 {}
@@ -283,7 +299,7 @@ def _get_child_sa_proposal(child_sa_data: dict) -> dict:
'mode': mode,
'key_size': key_size,
'hash': integ_alg,
- 'dh': dh_group
+ 'dh': dh_group,
}
return proposal
return {}
@@ -305,10 +321,10 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> 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)
+ 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['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')
@@ -322,22 +338,25 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> list:
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_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)
@@ -347,17 +366,16 @@ def _get_raw_connections_summary(list_conn, list_sas):
'tunnels': child,
'total': len(child),
'down': tunnels_down,
- 'up': tunnels_up
+ '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')
@@ -367,15 +385,26 @@ def _get_formatted_output_conections(data):
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
- ])
+ 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')
@@ -384,18 +413,36 @@ def _get_formatted_output_conections(data):
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
- ])
+ 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'
+ 'Connection',
+ 'State',
+ 'Type',
+ 'Remote address',
+ 'Local TS',
+ 'Remote TS',
+ 'Local id',
+ 'Remote id',
+ 'Proposal',
]
output = tabulate(connections, connection_headers, numalign='left')
return output
@@ -421,6 +468,31 @@ def _get_childsa_id_list(ike_sas: list) -> list:
return list_childsa_id
+def _get_con_childsa_name_list(
+ ike_sas: list, filter_dict: typing.Optional[dict] = None
+) -> list:
+ """
+ Generate list of CHILD SA ids based on list of OrderingDict
+ wich is returned by vici
+ :param ike_sas: list of IKE SAs connections generated by vici
+ :type ike_sas: list
+ :param filter_dict: dict of filter options
+ :type filter_dict: dict
+ :return: list of IKE SAs name
+ :rtype: list
+ """
+ list_childsa_name: list = []
+ for ike in ike_sas:
+ for ike_name, ike_values in ike.items():
+ for sa, sa_values in ike_values['children'].items():
+ if filter_dict:
+ if filter_dict.items() <= sa_values.items():
+ list_childsa_name.append(sa)
+ else:
+ list_childsa_name.append(sa)
+ return list_childsa_name
+
+
def _get_all_sitetosite_peers_name_list() -> list:
"""
Return site-to-site peers configuration
@@ -429,53 +501,142 @@ def _get_all_sitetosite_peers_name_list() -> list:
"""
conf: ConfigTreeQuery = ConfigTreeQuery()
config_path = ['vpn', 'ipsec', 'site-to-site', 'peer']
- peers_config = conf.get_config_dict(config_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+ peers_config = conf.get_config_dict(
+ config_path,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
peers_list: list = []
for name in peers_config:
peers_list.append(name)
return peers_list
-def reset_peer(peer: str, tunnel: typing.Optional[str] = None):
- # Convert tunnel to Strongwan format of CHILD_SA
+def _get_tunnel_sw_format(peer: str, tunnel: str) -> str:
+ """
+ Convert tunnel to Strongwan format of CHILD_SA
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ :return: Converted tunnel name (CHILD_SA)
+ :rtype: str
+ """
tunnel_sw = None
if tunnel:
if tunnel.isnumeric():
tunnel_sw = f'{peer}-tunnel-{tunnel}'
elif tunnel == 'vti':
tunnel_sw = f'{peer}-vti'
+ return tunnel_sw
+
+
+def _initiate_peer_with_childsas(
+ peer: str, tunnel: typing.Optional[str] = None
+) -> None:
+ """
+ Initiate IPSEC peer SAs by vici.
+ If tunnel is None it initiates all peers tunnels
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ """
+ tunnel_sw = _get_tunnel_sw_format(peer, tunnel)
try:
- sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw)
- if not sa_list:
+ con_list: list = vyos.ipsec.get_vici_connection_by_name(peer)
+ if not con_list:
raise vyos.opmode.IncorrectValue(
- f'Peer\'s {peer} SA(s) not found, aborting')
- if tunnel and sa_list:
- childsa_id_list: list = _get_childsa_id_list(sa_list)
- if not childsa_id_list:
- raise vyos.opmode.IncorrectValue(
- f'Peer {peer} tunnel {tunnel} SA(s) not found, aborting')
- vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
- print(f'Peer {peer} reset result: success')
- except (vyos.ipsec.ViciInitiateError) as err:
+ f"Peer's {peer} SA(s) not loaded. Initiation was failed"
+ )
+ childsa_name_list: list = _get_con_childsa_name_list(con_list)
+
+ if not tunnel_sw:
+ vyos.ipsec.vici_initiate_all_child_sa_by_ike(peer, childsa_name_list)
+ print(f'Peer {peer} initiate result: success')
+ return
+
+ if tunnel_sw in childsa_name_list:
+ vyos.ipsec.vici_initiate_all_child_sa_by_ike(peer, [tunnel_sw])
+ print(f'Peer {peer} tunnel {tunnel} initiate result: success')
+ return
+
+ raise vyos.opmode.IncorrectValue(f'Peer {peer} SA {tunnel} not found, aborting')
+
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciCommandError) as err:
+ except vyos.ipsec.ViciCommandError as err:
raise vyos.opmode.IncorrectValue(err)
-def reset_all_peers():
+def _terminate_peer(peer: str, tunnel: typing.Optional[str] = None) -> None:
+ """
+ Terminate IPSEC peer SAs by vici.
+ If tunnel is None it terminates all peers tunnels
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ """
+ # Convert tunnel to Strongwan format of CHILD_SA
+ tunnel_sw = _get_tunnel_sw_format(peer, tunnel)
+ try:
+ sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw)
+ if sa_list:
+ if tunnel:
+ childsa_id_list: list = _get_childsa_id_list(sa_list)
+ if childsa_id_list:
+ vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
+ print(f'Peer {peer} tunnel {tunnel} terminate result: success')
+ else:
+ Warning(
+ f'Peer {peer} tunnel {tunnel} SA is not initiated. Nothing to terminate'
+ )
+ else:
+ vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
+ print(f'Peer {peer} terminate result: success')
+ else:
+ Warning(f"Peer's {peer} SAs are not initiated. Nothing to terminate")
+
+ except vyos.ipsec.ViciInitiateError as err:
+ raise vyos.opmode.UnconfiguredSubsystem(err)
+ except vyos.ipsec.ViciCommandError as err:
+ raise vyos.opmode.IncorrectValue(err)
+
+
+def reset_peer(peer: str, tunnel: typing.Optional[str] = None) -> None:
+ """
+ Reset IPSEC peer SAs.
+ If tunnel is None it resets all peers tunnels
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ """
+ _terminate_peer(peer, tunnel)
+ peer_config = _get_sitetosite_peer_config(peer)
+ # initiate SAs only if 'connection-type=initiate'
+ if (
+ 'connection_type' in peer_config
+ and peer_config['connection_type'] == 'initiate'
+ ):
+ _initiate_peer_with_childsas(peer, tunnel)
+
+
+def reset_all_peers() -> None:
sitetosite_list = _get_all_sitetosite_peers_name_list()
if sitetosite_list:
for peer_name in sitetosite_list:
try:
reset_peer(peer_name)
- except (vyos.opmode.IncorrectValue) as err:
+ except vyos.opmode.IncorrectValue as err:
print(err)
print('Peers reset result: success')
else:
raise vyos.opmode.UnconfiguredSubsystem(
- 'VPN IPSec site-to-site is not configured, aborting')
+ 'VPN IPSec site-to-site is not configured, aborting'
+ )
def _get_ra_session_list_by_username(username: typing.Optional[str] = None):
@@ -500,7 +661,7 @@ def _get_ra_session_list_by_username(username: typing.Optional[str] = None):
def reset_ra(username: typing.Optional[str] = None):
- #Reset remote-access ipsec sessions
+ # Reset remote-access ipsec sessions
if username:
list_sa_id = _get_ra_session_list_by_username(username)
else:
@@ -514,32 +675,47 @@ def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str):
ike_sa_name = f'dmvpn-{profile}-{tunnel}'
try:
# Get IKE SAs
- sa_list = convert_data(
- vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
+ sa_list = convert_data(vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
if not sa_list:
raise vyos.opmode.IncorrectValue(
- f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting')
- sa_nbma_list = list([x for x in sa_list if
- ike_sa_name in x and x[ike_sa_name][
- 'remote-host'] == nbma_dst])
+ f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting'
+ )
+ sa_nbma_list = list(
+ [
+ x
+ for x in sa_list
+ if ike_sa_name in x and x[ike_sa_name]['remote-host'] == nbma_dst
+ ]
+ )
if not sa_nbma_list:
raise vyos.opmode.IncorrectValue(
- f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting')
+ f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting'
+ )
# terminate IKE SAs
- vyos.ipsec.terminate_vici_ikeid_list(list(
- [x[ike_sa_name]['uniqueid'] for x in sa_nbma_list if
- ike_sa_name in x]))
+ vyos.ipsec.terminate_vici_ikeid_list(
+ list(
+ [
+ x[ike_sa_name]['uniqueid']
+ for x in sa_nbma_list
+ if ike_sa_name in x
+ ]
+ )
+ )
# initiate IKE SAs
for ike in sa_nbma_list:
if ike_sa_name in ike:
- vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn',
- ike[ike_sa_name]['local-host'],
- ike[ike_sa_name]['remote-host'])
+ vyos.ipsec.vici_initiate(
+ ike_sa_name,
+ 'dmvpn',
+ ike[ike_sa_name]['local-host'],
+ ike[ike_sa_name]['remote-host'],
+ )
print(
- f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success')
- except (vyos.ipsec.ViciInitiateError) as err:
+ f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success'
+ )
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciCommandError) as err:
+ except vyos.ipsec.ViciCommandError as err:
raise vyos.opmode.IncorrectValue(err)
@@ -549,24 +725,30 @@ def reset_profile_all(profile: str, tunnel: str):
try:
# Get IKE SAs
sa_list: list = convert_data(
- vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
+ vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)
+ )
if not sa_list:
raise vyos.opmode.IncorrectValue(
- f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting')
+ f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting'
+ )
# terminate IKE SAs
vyos.ipsec.terminate_vici_by_name(ike_sa_name, None)
# initiate IKE SAs
for ike in sa_list:
if ike_sa_name in ike:
- vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn',
- ike[ike_sa_name]['local-host'],
- ike[ike_sa_name]['remote-host'])
+ vyos.ipsec.vici_initiate(
+ ike_sa_name,
+ 'dmvpn',
+ ike[ike_sa_name]['local-host'],
+ ike[ike_sa_name]['remote-host'],
+ )
print(
- f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success')
+ f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success'
+ )
print(f'Profile {profile} tunnel {tunnel} reset result: success')
- except (vyos.ipsec.ViciInitiateError) as err:
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciCommandError) as err:
+ except vyos.ipsec.ViciCommandError as err:
raise vyos.opmode.IncorrectValue(err)
@@ -734,36 +916,56 @@ def _get_formatted_output_ra_summary(ra_output_list: list):
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_state = 'UP'
sa_uptime = seconds_to_human(sa['established'])
else:
sa_ipsec_proposal = ''
- sa_state = "DOWN"
+ 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_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):
+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')
+ raise vyos.opmode.IncorrectValue('No active connections found, aborting')
if raw:
return list_sa
return _get_output_ra_sas_detail(list_sa)
@@ -772,8 +974,7 @@ def show_ra_detail(raw: bool, username: typing.Optional[str] = None,
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')
+ raise vyos.opmode.IncorrectValue('No active connections found, aborting')
if raw:
return list_sa
return _get_formatted_output_ra_summary(list_sa)
@@ -783,9 +984,12 @@ def show_ra_summary(raw: bool):
def _get_raw_psk():
conf: ConfigTreeQuery = ConfigTreeQuery()
config_path = ['vpn', 'ipsec', 'authentication', 'psk']
- psk_config = conf.get_config_dict(config_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+ psk_config = conf.get_config_dict(
+ config_path,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
psk_list = []
for psk, psk_data in psk_config.items():
@@ -796,11 +1000,13 @@ def _get_raw_psk():
def _get_formatted_psk(psk_list):
- headers = ["PSK", "Id", "Secret"]
+ headers = ['PSK', 'Id', 'Secret']
formatted_data = []
for psk_data in psk_list:
- formatted_data.append([psk_data["psk"], "\n".join(psk_data["id"]), psk_data["secret"]])
+ formatted_data.append(
+ [psk_data['psk'], '\n'.join(psk_data['id']), psk_data['secret']]
+ )
return tabulate(formatted_data, headers=headers)
@@ -808,16 +1014,36 @@ def _get_formatted_psk(psk_list):
def show_psk(raw: bool):
config = ConfigTreeQuery()
if not config.exists('vpn ipsec authentication psk'):
- raise vyos.opmode.UnconfiguredSubsystem('VPN ipsec psk authentication is not configured')
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'VPN ipsec psk authentication is not configured'
+ )
psk = _get_raw_psk()
if raw:
return psk
return _get_formatted_psk(psk)
+
# PSK block end
+def _get_sitetosite_peer_config(peer: str):
+ """
+ Return site-to-site peers configuration
+ :return: site-to-site peers configuration
+ :rtype: list
+ """
+ conf: ConfigTreeQuery = ConfigTreeQuery()
+ config_path = ['vpn', 'ipsec', 'site-to-site', 'peer', peer]
+ peers_config = conf.get_config_dict(
+ config_path,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ return peers_config
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 16a545cda..c6cf4770a 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -31,6 +31,7 @@ from vyos.utils.dict import dict_search
ArgDirection = typing.Literal['source', 'destination']
ArgFamily = typing.Literal['inet', 'inet6']
+
def _get_xml_translation(direction, family, address=None):
"""
Get conntrack XML output --src-nat|--dst-nat
@@ -99,22 +100,35 @@ def _get_raw_translation(direction, family, address=None):
def _get_formatted_output_rules(data, direction, family):
- def _get_ports_for_output(my_dict):
- # Get and insert all configured ports or port ranges into output string
- for index, port in enumerate(my_dict['set']):
- if 'range' in str(my_dict['set'][index]):
- output = my_dict['set'][index]['range']
- output = '-'.join(map(str, output))
- else:
- output = str(port)
- if index == 0:
- output = str(output)
- else:
- output = ','.join([output,output])
- # Handle case where configured ports are a negated list
- if my_dict['op'] == '!=':
- output = '!' + output
- return(output)
+
+
+ def _get_ports_for_output(rules):
+ """
+ Return: string of configured ports
+ """
+ ports = []
+ if 'set' in rules:
+ for index, port in enumerate(rules['set']):
+ if 'range' in str(rules['set'][index]):
+ output = rules['set'][index]['range']
+ output = '-'.join(map(str, output))
+ else:
+ output = str(port)
+ ports.append(output)
+ # When NAT rule contains port range or single port
+ # JSON will not contain keyword 'set'
+ elif 'range' in rules:
+ output = rules['range']
+ output = '-'.join(map(str, output))
+ ports.append(output)
+ else:
+ output = rules['right']
+ ports.append(str(output))
+ result = ','.join(ports)
+ # Handle case where ports in NAT rule are negated
+ if rules['op'] == '!=':
+ result = '!' + result
+ return(result)
# Add default values before loop
sport, dport, proto = 'any', 'any', 'any'
@@ -132,7 +146,10 @@ def _get_formatted_output_rules(data, direction, family):
if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)):
if 'payload' in match['left']:
- if isinstance(match['right'], dict) and ('prefix' in match['right'] or 'set' in match['right']):
+ # Handle NAT rule containing comma-seperated list of ports
+ if (isinstance(match['right'], dict) and
+ ('prefix' in match['right'] or 'set' in match['right'] or
+ 'range' in match['right'])):
# Merge dict src/dst l3_l4 parameters
my_dict = {**match['left']['payload'], **match['right']}
my_dict['op'] = match['op']
@@ -146,6 +163,7 @@ def _get_formatted_output_rules(data, direction, family):
sport = _get_ports_for_output(my_dict)
elif my_dict['field'] == 'dport':
dport = _get_ports_for_output(my_dict)
+ # Handle NAT rule containing a single port
else:
field = jmespath.search('left.payload.field', match)
if field == 'saddr':
@@ -153,9 +171,9 @@ def _get_formatted_output_rules(data, direction, family):
elif field == 'daddr':
daddr = match.get('right')
elif field == 'sport':
- sport = match.get('right')
+ sport = _get_ports_for_output(match)
elif field == 'dport':
- dport = match.get('right')
+ dport = _get_ports_for_output(match)
else:
saddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
daddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py
index cfa0678a7..62c683ebb 100755
--- a/src/op_mode/openconnect.py
+++ b/src/op_mode/openconnect.py
@@ -42,8 +42,10 @@ def _get_formatted_sessions(data):
ses_list = []
for ses in data:
ses_list.append([
- ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"],
- ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]
+ ses.get("Device", '(none)'), ses.get("Username", '(none)'),
+ ses.get("IPv4", '(none)'), ses.get("Remote IP", '(none)'),
+ ses.get("_RX", '(none)'), ses.get("_TX", '(none)'),
+ ses.get("State", '(none)'), ses.get("_Connected at", '(none)')
])
if len(ses_list) > 0:
output = tabulate(ses_list, headers)
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 9ce166c7d..84b080023 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -844,7 +844,8 @@ def import_openvpn_secret(name, path):
key_version = '1'
with open(path) as f:
- key_lines = f.read().split("\n")
+ key_lines = f.read().strip().split("\n")
+ key_lines = list(filter(lambda line: not line.strip().startswith('#'), key_lines)) # Remove commented lines
key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully)
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index cb4a175dd..c32a2be7d 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -24,7 +24,6 @@ from time import time
from vyos.utils.io import ask_yes_no
from vyos.utils.process import call
-from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.utils.process import STDOUT
@@ -117,11 +116,15 @@ def check_unsaved_config():
pass
def execute_shutdown(time, reboot=True, ask=True):
+ from vyos.utils.process import cmd
+
check_unsaved_config()
+ host = cmd("hostname --fqdn")
+
action = "reboot" if reboot else "poweroff"
if not ask:
- if not ask_yes_no(f"Are you sure you want to {action} this system?"):
+ if not ask_yes_no(f"Are you sure you want to {action} this system ({host})?"):
exit(0)
action_cmd = "-r" if reboot else "-P"
diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py
new file mode 100755
index 000000000..813d3a2b7
--- /dev/null
+++ b/src/op_mode/restart.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 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 typing
+import vyos.opmode
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import call
+from vyos.utils.commit import commit_in_progress
+
+config = ConfigTreeQuery()
+
+service_map = {
+ 'dhcp' : {
+ 'systemd_service': 'kea-dhcp4-server',
+ 'path': ['service', 'dhcp-server'],
+ },
+ 'dhcpv6' : {
+ 'systemd_service': 'kea-dhcp6-server',
+ 'path': ['service', 'dhcpv6-server'],
+ },
+ 'dns_dynamic': {
+ 'systemd_service': 'ddclient',
+ 'path': ['service', 'dns', 'dynamic'],
+ },
+ 'dns_forwarding': {
+ 'systemd_service': 'pdns-recursor',
+ 'path': ['service', 'dns', 'forwarding'],
+ },
+ 'igmp_proxy': {
+ 'systemd_service': 'igmpproxy',
+ 'path': ['protocols', 'igmp-proxy'],
+ },
+ 'ipsec': {
+ 'systemd_service': 'strongswan',
+ 'path': ['vpn', 'ipsec'],
+ },
+ 'mdns_repeater': {
+ 'systemd_service': 'avahi-daemon',
+ 'path': ['service', 'mdns', 'repeater'],
+ },
+ 'reverse_proxy': {
+ 'systemd_service': 'haproxy',
+ 'path': ['load-balancing', 'reverse-proxy'],
+ },
+ 'router_advert': {
+ 'systemd_service': 'radvd',
+ 'path': ['service', 'router-advert'],
+ },
+ 'snmp' : {
+ 'systemd_service': 'snmpd',
+ },
+ 'ssh' : {
+ 'systemd_service': 'ssh',
+ },
+ 'suricata' : {
+ 'systemd_service': 'suricata',
+ },
+ 'vrrp' : {
+ 'systemd_service': 'keepalived',
+ 'path': ['high-availability', 'vrrp'],
+ },
+ 'webproxy' : {
+ 'systemd_service': 'squid',
+ },
+}
+services = typing.Literal['dhcp', 'dhcpv6', 'dns_dynamic', 'dns_forwarding', 'igmp_proxy', 'ipsec', 'mdns_repeater', 'reverse_proxy', 'router_advert', 'snmp', 'ssh', 'suricata' 'vrrp', 'webproxy']
+
+def _verify(func):
+ """Decorator checks if DHCP(v6) config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ name = kwargs.get('name')
+ human_name = name.replace('_', '-')
+
+ if commit_in_progress():
+ print(f'Cannot restart {human_name} service while a commit is in progress')
+ sys.exit(1)
+
+ # Get optional CLI path from service_mapping dict
+ # otherwise use "service name" CLI path
+ path = ['service', name]
+ if 'path' in service_map[name]:
+ path = service_map[name]['path']
+
+ # Check if config does not exist
+ if not config.exists(path):
+ raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is not configured!')
+ if config.exists(path + ['disable']):
+ raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is disabled!')
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+@_verify
+def restart_service(raw: bool, name: services, vrf: typing.Optional[str]):
+ systemd_service = service_map[name]['systemd_service']
+ if vrf:
+ call(f'systemctl restart "{systemd_service}@{vrf}.service"')
+ else:
+ call(f'systemctl restart "{systemd_service}.service"')
+
+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/serial.py b/src/op_mode/serial.py
new file mode 100644
index 000000000..a5864872b
--- /dev/null
+++ b/src/op_mode/serial.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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, typing
+
+import vyos.opmode
+from vyos.utils.serial import restart_login_consoles as _restart_login_consoles
+
+def restart_console(device_name: typing.Optional[str]):
+ # Service control moved to vyos.utils.serial to unify checks and prompts.
+ # If users are connected, we want to show an informational message and a prompt
+ # to continue, verifying that the user acknowledges possible interruptions.
+ if device_name:
+ _restart_login_consoles(prompt_user=True, quiet=False, devices=[device_name])
+ else:
+ _restart_login_consoles(prompt_user=True, quiet=False)
+
+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/ssh.py b/src/op_mode/ssh.py
index 102becc55..0c51576b0 100755
--- a/src/op_mode/ssh.py
+++ b/src/op_mode/ssh.py
@@ -65,7 +65,7 @@ def show_fingerprints(raw: bool, ascii: bool):
def show_dynamic_protection(raw: bool):
config = ConfigTreeQuery()
if not config.exists(['service', 'ssh', 'dynamic-protection']):
- raise vyos.opmode.UnconfiguredSubsystem("SSH server dynamic-protection is not enabled.")
+ raise vyos.opmode.UnconfiguredObject("SSH server dynamic-protection is not enabled.")
attackers = []
try:
diff --git a/src/op_mode/tech_support.py b/src/op_mode/tech_support.py
new file mode 100644
index 000000000..f60bb87ff
--- /dev/null
+++ b/src/op_mode/tech_support.py
@@ -0,0 +1,394 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 json
+
+import vyos.opmode
+
+from vyos.utils.process import cmd
+
+def _get_version_data():
+ from vyos.version import get_version_data
+ return get_version_data()
+
+def _get_uptime():
+ from vyos.utils.system import get_uptime_seconds
+
+ return get_uptime_seconds()
+
+def _get_load_average():
+ from vyos.utils.system import get_load_averages
+
+ return get_load_averages()
+
+def _get_cpus():
+ from vyos.utils.cpu import get_cpus
+
+ return get_cpus()
+
+def _get_process_stats():
+ return cmd('top --iterations 1 --batch-mode --accum-time-toggle')
+
+def _get_storage():
+ from vyos.utils.disk import get_persistent_storage_stats
+
+ return get_persistent_storage_stats()
+
+def _get_devices():
+ devices = {}
+ devices["pci"] = cmd("lspci")
+ devices["usb"] = cmd("lsusb")
+
+ return devices
+
+def _get_memory():
+ from vyos.utils.file import read_file
+
+ return read_file("/proc/meminfo")
+
+def _get_processes():
+ res = cmd("ps aux")
+
+ return res
+
+def _get_interrupts():
+ from vyos.utils.file import read_file
+
+ interrupts = read_file("/proc/interrupts")
+ softirqs = read_file("/proc/softirqs")
+
+ return (interrupts, softirqs)
+
+def _get_partitions():
+ # XXX: as of parted 3.5, --json is completely broken
+ # and cannot be used (outputs malformed JSON syntax)
+ res = cmd(f"parted --list")
+
+ return res
+
+def _get_running_config():
+ from os import getpid
+ from vyos.configsession import ConfigSession
+ from vyos.utils.strip_config import strip_config_source
+
+ c = ConfigSession(getpid())
+ return strip_config_source(c.show_config([]))
+
+def _get_boot_config():
+ from vyos.utils.file import read_file
+ from vyos.utils.strip_config import strip_config_source
+
+ config = read_file('/opt/vyatta/etc/config.boot.default')
+
+ return strip_config_source(config)
+
+def _get_config_scripts():
+ from os import listdir
+ from os.path import join
+ from vyos.utils.file import read_file
+
+ scripts = []
+
+ dir = '/config/scripts'
+ for f in listdir(dir):
+ script = {}
+ path = join(dir, f)
+ data = read_file(path)
+ script["path"] = path
+ script["data"] = data
+
+ scripts.append(script)
+
+ return scripts
+
+def _get_nic_data():
+ from vyos.utils.process import ip_cmd
+ link_data = ip_cmd("link show")
+ addr_data = ip_cmd("address show")
+
+ return link_data, addr_data
+
+def _get_routes(proto):
+ from json import loads
+ from vyos.utils.process import ip_cmd
+
+ # Only include complete routing tables if they are not too large
+ # At the moment "too large" is arbitrarily set to 1000
+ MAX_ROUTES = 1000
+
+ data = {}
+
+ summary = cmd(f"vtysh -c 'show {proto} route summary json'")
+ summary = loads(summary)
+
+ data["summary"] = summary
+
+ if summary["routesTotal"] < MAX_ROUTES:
+ rib_routes = cmd(f"vtysh -c 'show {proto} route json'")
+ data["routes"] = loads(rib_routes)
+
+ if summary["routesTotalFib"] < MAX_ROUTES:
+ ip_proto = "-4" if proto == "ip" else "-6"
+ fib_routes = ip_cmd(f"{ip_proto} route show")
+ data["fib_routes"] = fib_routes
+
+ return data
+
+def _get_ip_routes():
+ return _get_routes("ip")
+
+def _get_ipv6_routes():
+ return _get_routes("ipv6")
+
+def _get_ospfv2():
+ # XXX: OSPF output when it's not configured is an empty string,
+ # which is not a valid JSON
+ output = cmd("vtysh -c 'show ip ospf json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_ospfv3():
+ output = cmd("vtysh -c 'show ipv6 ospf6 json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_bgp_summary():
+ output = cmd("vtysh -c 'show bgp summary json'")
+ return json.loads(output)
+
+def _get_isis():
+ output = cmd("vtysh -c 'show isis summary json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_arp_table():
+ from json import loads
+ from vyos.utils.process import cmd
+
+ arp_table = cmd("ip --json -4 neighbor show")
+ return loads(arp_table)
+
+def _get_ndp_table():
+ from json import loads
+
+ arp_table = cmd("ip --json -6 neighbor show")
+ return loads(arp_table)
+
+def _get_nftables_rules():
+ nft_rules = cmd("nft list ruleset")
+ return nft_rules
+
+def _get_connections():
+ from vyos.utils.process import cmd
+
+ return cmd("ss -apO")
+
+def _get_system_packages():
+ from re import split
+ from vyos.utils.process import cmd
+
+ dpkg_out = cmd(''' dpkg-query -W -f='${Package} ${Version} ${Architecture} ${db:Status-Abbrev}\n' ''')
+ pkg_lines = split(r'\n+', dpkg_out)
+
+ # Discard the header, it's five lines long
+ pkg_lines = pkg_lines[5:]
+
+ pkgs = []
+
+ for pl in pkg_lines:
+ parts = split(r'\s+', pl)
+ pkg = {}
+ pkg["name"] = parts[0]
+ pkg["version"] = parts[1]
+ pkg["architecture"] = parts[2]
+ pkg["status"] = parts[3]
+
+ pkgs.append(pkg)
+
+ return pkgs
+
+def _get_image_info():
+ from vyos.system.image import get_images_details
+
+ return get_images_details()
+
+def _get_kernel_modules():
+ from vyos.utils.kernel import lsmod
+
+ return lsmod()
+
+def _get_last_logs(max):
+ from systemd import journal
+
+ r = journal.Reader()
+
+ # Set the reader to use logs from the current boot
+ r.this_boot()
+
+ # Jump to the last logs
+ r.seek_tail()
+
+ # Only get logs of INFO level or more urgent
+ r.log_level(journal.LOG_INFO)
+
+ # Retrieve the entries
+ entries = []
+
+ # I couldn't find a way to just get last/first N entries,
+ # so we'll use the cursor directly.
+ num = max
+ while num >= 0:
+ je = r.get_previous()
+ entry = {}
+
+ # Extract the most useful and serializable fields
+ entry["timestamp"] = je.get("SYSLOG_TIMESTAMP")
+ entry["pid"] = je.get("SYSLOG_PID")
+ entry["identifier"] = je.get("SYSLOG_IDENTIFIER")
+ entry["facility"] = je.get("SYSLOG_FACILITY")
+ entry["systemd_unit"] = je.get("_SYSTEMD_UNIT")
+ entry["message"] = je.get("MESSAGE")
+
+ entries.append(entry)
+
+ num = num - 1
+
+ return entries
+
+
+def _get_raw_data():
+ data = {}
+
+ # VyOS-specific information
+ data["vyos"] = {}
+
+ ## The equivalent of "show version"
+ from vyos.version import get_version_data
+ data["vyos"]["version"] = _get_version_data()
+
+ ## Installed images
+ data["vyos"]["images"] = _get_image_info()
+
+ # System information
+ data["system"] = {}
+
+ ## Uptime and load averages
+ data["system"]["uptime"] = _get_uptime()
+ data["system"]["load_average"] = _get_load_average()
+ data["system"]["process_stats"] = _get_process_stats()
+
+ ## Debian packages
+ data["system"]["packages"] = _get_system_packages()
+
+ ## Kernel modules
+ data["system"]["kernel"] = {}
+ data["system"]["kernel"]["modules"] = _get_kernel_modules()
+
+ ## Processes
+ data["system"]["processes"] = _get_processes()
+
+ ## Interrupts
+ interrupts, softirqs = _get_interrupts()
+ data["system"]["interrupts"] = interrupts
+ data["system"]["softirqs"] = softirqs
+
+ # Hardware
+ data["hardware"] = {}
+ data["hardware"]["cpu"] = _get_cpus()
+ data["hardware"]["storage"] = _get_storage()
+ data["hardware"]["partitions"] = _get_partitions()
+ data["hardware"]["devices"] = _get_devices()
+ data["hardware"]["memory"] = _get_memory()
+
+ # Configuration data
+ data["vyos"]["config"] = {}
+
+ ## Running config text
+ ## We do not encode it so that it's possible to
+ ## see exactly what the user sees and detect any syntax/rendering anomalies —
+ ## exporting the config to JSON could obscure them
+ data["vyos"]["config"]["running"] = _get_running_config()
+
+ ## Default boot config, exactly as in /config/config.boot
+ ## It may be different from the running config
+ ## _and_ may have its own syntax quirks that may point at bugs
+ data["vyos"]["config"]["boot"] = _get_boot_config()
+
+ ## Config scripts
+ data["vyos"]["config"]["scripts"] = _get_config_scripts()
+
+ # Network interfaces
+ data["network_interfaces"] = {}
+
+ # Interface data from iproute2
+ link_data, addr_data = _get_nic_data()
+ data["network_interfaces"]["links"] = link_data
+ data["network_interfaces"]["addresses"] = addr_data
+
+ # Routing table data
+ data["routing"] = {}
+ data["routing"]["ip"] = _get_ip_routes()
+ data["routing"]["ipv6"] = _get_ipv6_routes()
+
+ # Routing protocols
+ data["routing"]["ip"]["ospf"] = _get_ospfv2()
+ data["routing"]["ipv6"]["ospfv3"] = _get_ospfv3()
+
+ data["routing"]["bgp"] = {}
+ data["routing"]["bgp"]["summary"] = _get_bgp_summary()
+
+ data["routing"]["isis"] = _get_isis()
+
+ # ARP and NDP neighbor tables
+ data["neighbor_tables"] = {}
+ data["neighbor_tables"]["arp"] = _get_arp_table()
+ data["neighbor_tables"]["ndp"] = _get_ndp_table()
+
+ # nftables config
+ data["nftables_rules"] = _get_nftables_rules()
+
+ # All connections
+ data["connections"] = _get_connections()
+
+ # Logs
+ data["last_logs"] = _get_last_logs(1000)
+
+ return data
+
+def show(raw: bool):
+ data = _get_raw_data()
+ if raw:
+ return data
+ else:
+ raise vyos.opmode.UnsupportedOperation("Formatted output is not implemented yet")
+
+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)
+ except (KeyboardInterrupt, BrokenPipeError):
+ sys.exit(1)
diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py
index d24b1065b..49fecdf28 100644
--- a/src/op_mode/zone.py
+++ b/src/op_mode/zone.py
@@ -104,7 +104,7 @@ def _convert_config(zones_config: dict, zone: str = None) -> list:
if zones_config:
output = [_convert_one_zone_data(zone, zones_config)]
else:
- raise vyos.opmode.DataUnavailable(f'Zone {zone} not found')
+ raise vyos.opmode.UnconfiguredObject(f'Zone {zone} not found')
else:
if zones_config:
output = _convert_zones_data(zones_config)
@@ -212,4 +212,4 @@ if __name__ == '__main__':
print(res)
except (ValueError, vyos.opmode.Error) as e:
print(e)
- sys.exit(1) \ No newline at end of file
+ sys.exit(1)
diff --git a/src/opt/vyatta/bin/restricted-shell b/src/opt/vyatta/bin/restricted-shell
new file mode 100755
index 000000000..ffcbb53b7
--- /dev/null
+++ b/src/opt/vyatta/bin/restricted-shell
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+if [ $# != 0 ]; then
+ echo "Remote command execution is not allowed for operator level users"
+ args=($@)
+ args_str=$(IFS=" " ; echo "${args[*]}")
+ logger "Operator level user $USER attempted remote command execution: $args_str"
+ exit 1
+fi
+
+exec vbash
diff --git a/src/opt/vyatta/bin/vyatta-op-cmd-wrapper b/src/opt/vyatta/bin/vyatta-op-cmd-wrapper
new file mode 100755
index 000000000..a89211b2b
--- /dev/null
+++ b/src/opt/vyatta/bin/vyatta-op-cmd-wrapper
@@ -0,0 +1,6 @@
+#!/bin/vbash
+shopt -s expand_aliases
+source /etc/default/vyatta
+source /etc/bash_completion.d/vyatta-op
+_vyatta_op_init
+_vyatta_op_run "$@"
diff --git a/src/opt/vyatta/etc/LICENSE b/src/opt/vyatta/etc/LICENSE
new file mode 100644
index 000000000..6d45519c8
--- /dev/null
+++ b/src/opt/vyatta/etc/LICENSE
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ 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, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/src/opt/vyatta/etc/shell/level/users/allowed-op b/src/opt/vyatta/etc/shell/level/users/allowed-op
new file mode 100644
index 000000000..74c45af37
--- /dev/null
+++ b/src/opt/vyatta/etc/shell/level/users/allowed-op
@@ -0,0 +1,20 @@
+c
+cl
+cle
+clea
+clear
+connect
+delete
+disconnect
+exit
+force
+monitor
+ping
+reset
+release
+renew
+set
+show
+telnet
+traceroute
+update
diff --git a/src/opt/vyatta/etc/shell/level/users/allowed-op.in b/src/opt/vyatta/etc/shell/level/users/allowed-op.in
new file mode 100644
index 000000000..1976904e4
--- /dev/null
+++ b/src/opt/vyatta/etc/shell/level/users/allowed-op.in
@@ -0,0 +1,16 @@
+clear
+connect
+delete
+disconnect
+exit
+force
+monitor
+ping
+reset
+release
+renew
+set
+show
+telnet
+traceroute
+update
diff --git a/src/opt/vyatta/sbin/if-mib-alias b/src/opt/vyatta/sbin/if-mib-alias
new file mode 100755
index 000000000..bc86f999f
--- /dev/null
+++ b/src/opt/vyatta/sbin/if-mib-alias
@@ -0,0 +1,130 @@
+#! /usr/bin/perl
+
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Stephen Hemminger
+# Date: October 2010
+# Description: script is run as net-snmp extension to read interface alias
+#
+# **** End License ****
+
+use strict;
+use warnings;
+use feature "switch";
+no warnings 'experimental::smartmatch';
+
+# Collect interface all alias values
+sub get_alias {
+ my @interfaces;
+
+ open (my $ip, '-|', 'ip li')
+ or die "Can't run ip command\n";
+ my $index;
+ while(<$ip>) {
+ if (/^(\d+): ([^:]*): /) {
+ $index = $1;
+ $interfaces[$index] = $2;
+ } elsif (/^ +alias (.*)$/) {
+ $interfaces[$index] = $1;
+ }
+ }
+ close $ip;
+ return @interfaces;
+}
+
+sub get_oid {
+ my $oid = shift;
+ die "Not a valid Object ID: $oid"
+ unless ($oid =~ /.(\d+)$/);
+
+ my $ifindex = $1;
+ my @interfaces = get_alias();
+
+ my $ifalias = $interfaces[$ifindex];
+ print "$oid\nstring\n$ifalias\n" if $ifalias;
+}
+
+# OID of ifAlias [RFC2863]
+my $BASE = '.1.3.6.1.2.1.31.1.1.1.18';
+
+sub get_next {
+ my $oid = shift;
+
+ return get_next("$BASE.0")
+ if ($oid eq $BASE);
+
+ die "Not a valid Object ID: $oid"
+ unless ($oid =~ /^(\S*)\.(\d+)$/);
+
+ my $base = $1;
+ my $ifindex = $2;
+ my @interfaces = get_alias();
+
+ while (++$ifindex <= $#interfaces) {
+ my $ifalias = $interfaces[$ifindex];
+ if ($ifalias) {
+ print "$base.$ifindex\nstring\n$ifalias\n";
+ last;
+ }
+ }
+}
+
+sub ifindextoname {
+ my $ifindex = shift;
+
+ open (my $ip, '-|', 'ip li')
+ or die "Can't run ip command\n";
+ my $index;
+ while(<$ip>) {
+ next unless (/^(\d+): ([^:]*): /);
+ return $2 if ($1 == $ifindex);
+ }
+ return;
+}
+
+sub set_oid {
+ my ($oid, $target, $value) = @_;
+ die "Not a valid Object ID: $oid"
+ unless ($oid =~ /\.(\d+)$/);
+ my $ifindex = $1;
+ unless ($target eq 'string') {
+ print "wrong-type\n";
+ return;
+ }
+
+ my $ifname = ifindextoname($ifindex);
+ if ($ifname) {
+ system("ip li set $ifname alias '$value' >/dev/null 2>&1");
+ print "not-writeable\n" if ($? != 0);
+ }
+}
+
+sub usage {
+ warn "Usage: $0 {-g|-n} OID\n";
+ warn " $0 -s OID TARGET VALUE\n";
+ exit 1;
+}
+
+usage unless $#ARGV >= 1;
+
+given ($ARGV[0]) {
+ when ('-g') { get_oid ($ARGV[1]); }
+ when ('-n') { get_next ($ARGV[1]); }
+ when ('-s') { set_oid ($ARGV[1], $ARGV[2], $ARGV[3]); }
+ default {
+ warn "$ARGV[0] unknown flag\n";
+ usage;
+ }
+}
diff --git a/src/opt/vyatta/sbin/vyos-persistpath b/src/opt/vyatta/sbin/vyos-persistpath
new file mode 100755
index 000000000..d7199b09a
--- /dev/null
+++ b/src/opt/vyatta/sbin/vyos-persistpath
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then
+ # Live CD boot
+ exit 2
+
+elif grep -q 'upperdir=/live/persistence/' /proc/mounts && egrep -q 'overlay / overlay ' /proc/mounts; then
+ # union boot
+
+ boot_device=`grep -o 'upperdir=/live/persistence/[^/]*/boot' /proc/mounts | cut -d / -f 4`
+ persist_path="/lib/live/mount/persistence/$boot_device"
+
+ echo $persist_path
+ exit 0
+else
+ # old style boot
+
+ exit 1
+fi \ No newline at end of file
diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common
new file mode 100644
index 000000000..e749f0217
--- /dev/null
+++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common
@@ -0,0 +1,82 @@
+# vyatta bash completion common functions
+
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# A copy of the GNU General Public License is available as
+# `/usr/share/common-licenses/GPL' in the Debian GNU/Linux distribution
+# or on the World Wide Web at `http://www.gnu.org/copyleft/gpl.html'.
+# You can also obtain it by writing to the Free Software Foundation,
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
+# MA 02110-1301, USA.
+#
+# Author: Vyatta
+# Description: bash completion common functions
+#
+# **** End License ****
+
+get_prefix_filtered_list ()
+{
+ # $1: prefix
+ # $2: \@list
+ # $3: \@filtered
+ declare -a olist
+ local pfx=$1
+ pfx=${pfx#\"}
+ eval "olist=( \"\${$2[@]}\" )"
+ local idx=0
+ for elem in "${olist[@]}"; do
+ local sub="${elem#$pfx}"
+ if [[ "$elem" == "$sub" ]] && [[ -n "$pfx" ]]; then
+ continue
+ fi
+ eval "$3[$idx]=\$elem"
+ (( idx++ ))
+ done
+}
+
+get_prefix_filtered_list2 ()
+{
+ # $1: prefix
+ # $2: \@list
+ # $3: \@filtered
+ # $4: \@list2
+ # $5: \@filtered2
+ declare -a olist
+ local pfx=$1
+ pfx=${pfx#\"}
+ eval "olist=( \"\${$2[@]}\" )"
+ eval "local orig_len=\${#$2[@]}"
+ local orig_idx=0
+ local idx=0
+ for (( orig_idx = 0; orig_idx < orig_len; orig_idx++ )); do
+ eval "local elem=\${$2[$orig_idx]}"
+ eval "local elem2=\${$4[$orig_idx]}"
+ local sub="${elem#$pfx}"
+ if [[ "$elem" == "$sub" ]] && [[ -n "$pfx" ]]; then
+ continue
+ fi
+ eval "$3[$idx]=\$elem"
+ eval "$5[$idx]=\$elem2"
+ (( idx++ ))
+ done
+}
+
+is_elem_of () {
+ local elem="$1"
+ local -a olist
+ eval "olist=( \"\${$2[@]}\" )"
+ for e in "${olist[@]}"; do
+ if [[ "$e" == "$elem" ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run
new file mode 100644
index 000000000..f0479ae88
--- /dev/null
+++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run
@@ -0,0 +1,240 @@
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Tom Grennan
+# Date: 2007
+# Description: setup bash completion for Vyatta operational commands
+#
+# **** End License ****
+
+_vyatta_op_init ()
+{
+ # empty and default line compeletion
+ complete -E -F _vyatta_op_expand
+ complete -D -F _vyatta_op_default_expand
+
+ # create the top level aliases for the unambiguous portions of the commands
+ # this is the only place we need an entire enumerated list of the subcommands
+ for cmd in $( ls /opt/vyatta/share/vyatta-op/templates/ ); do
+ for pos in $(seq 1 ${#cmd}); do
+ case ${cmd:0:$pos} in
+ for|do|done|if|fi|case|while|tr )
+ continue ;;
+ *) ;;
+ esac
+ complete -F _vyatta_op_expand ${cmd:0:$pos}
+ eval alias ${cmd:0:$pos}=\'_vyatta_op_run ${cmd:0:$pos}\'
+ done
+ done
+
+ shopt -s histverify
+}
+
+_vyatta_op_get_node_def_field ()
+{
+ local file=$1 field=$2
+
+ sed -n '/^'"$field"':/,$ {
+# strip field name and hold rest of line
+ s/[a-z]*: *//
+ h
+ :b
+# at EOF, print hold buffer and quit
+ $ { x; p; q }
+# input next line
+ n
+# if start of another field def, print hold buf and quit
+ /^[a-z]*:/ { x; p; q }
+# add to hold buf and branch to input next line
+ H
+ bb
+ }' $file
+}
+
+_vyatta_op_conv_node_path ()
+{
+ # is the node ok, ambiguous, or invalid
+ local node_path
+ local node
+ local -a ARR
+ node_path=$1
+ node=$2
+ ARR=( $(compgen -d $node_path/$node) )
+ if [[ "${#ARR[@]}" == "1" ]]; then
+ echo ${ARR[0]##*/}
+ elif [[ "${#ARR[@]}" == "0" ]]; then
+ if [[ -d "${node_path}/node.tag" ]]; then
+ echo "$node tag"
+ else
+ echo "$node invalid"
+ fi
+ elif [[ -d "$node_path/$node" ]]; then
+ echo $node
+ elif [[ "$VYATTA_USER_LEVEL_DIR" != "/opt/vyatta/etc/shell/level/admin" ]];then
+ # special handling for unprivledged completions.
+ # Since top level commands are different for unprivledged users
+ # we need a handler to expand them properly.
+ local -a filtered_cmds=()
+ local -a allowed=( $(cat $VYATTA_USER_LEVEL_DIR/allowed-op.in) )
+ get_prefix_filtered_list $node allowed filtered_cmds
+ if [[ "${#filtered_cmds[@]}" == "1" ]];then
+ echo ${filtered_cmds[0]}
+ else
+ echo "${node} ambiguous"
+ fi
+ else
+ echo "$node ambiguous"
+ fi
+}
+
+_vyatta_op_conv_run_cmd ()
+{
+ # Substitue bash positional variables
+ # for the same value in the expanded array
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -s extglob
+ shopt -u nullglob
+ local run_cmd="$1"
+ local line outline
+ local -i inquote=0;
+ local outcmd='';
+ local OIFS=$IFS
+ local re="([^']*')(.*)"
+
+ toggle_inquote()
+ {
+ if [[ $inquote == 0 ]]; then
+ inquote=1
+ else
+ inquote=0
+ fi
+ }
+
+ process_subline()
+ {
+ if [[ $inquote == 1 ]]; then
+ outline+="$1"
+ else
+ outline+=$(sed -e 's/\$\([0-9]\)/\$\{args\[\1\]\}/g' <<<"$1")
+ fi
+ }
+
+ run_cmd="${run_cmd/\"\$\@\"/${args[*]}}"
+ run_cmd="${run_cmd/\$\*/${args[*]}}"
+ run_cmd="${run_cmd//\\/\\\\}"
+ IFS=$'\n'
+ for line in ${run_cmd[@]}; do
+ outline=''
+ while [[ -n "$line" ]]; do
+ if [[ "$line" =~ $re ]]; then
+ process_subline "${BASH_REMATCH[1]}"
+ toggle_inquote
+ else
+ process_subline "$line"
+ fi
+ line="${BASH_REMATCH[2]}"
+ done
+ outcmd+="$outline\n"
+ done
+ IFS=$OIFS
+ eval "$restore_shopts"
+ echo -ne "$outcmd"
+}
+
+_vyatta_op_run ()
+{
+ # if run with bash builtin "set -/+*" run set and return
+ # this happens when a different completion script runs eval "set ..."
+ # (VyOS T1604)
+ if [[ "$1" == "set" && "$2" =~ ^(-|\+).* ]]; then
+ set "${@:2}"
+ return
+ fi
+
+ local -i estat
+ local tpath=$vyatta_op_templates
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -s extglob nullglob
+
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ false; estat=$?
+ stty echo 2> /dev/null # turn echo on, this is a workaround for bug 7570
+ # not a fix we need to look at why the readline library
+ # is getting confused on paged help text.
+
+ i=1
+ declare -a args # array of expanded arguments
+ for arg in "$@"; do
+ local orig_arg=$arg
+ if [[ $arg == "*" ]]; then
+ arg="*" #leave user defined wildcards alone
+ else
+ arg=( $(_vyatta_op_conv_node_path $tpath $arg) ) # expand the arguments
+ fi
+ # output proper error message based on the above expansion
+ if [[ "${arg[1]}" == "ambiguous" ]]; then
+ echo -ne "\n Ambiguous command: ${args[@]} [$arg]\n" >&2
+ local -a cmds=( $(compgen -d $tpath/$arg) )
+ _vyatta_op_node_path=$tpath
+ local comps=$(_vyatta_op_help $arg ${cmds[@]##*/})
+ echo -e "$comps\n" | sed -e 's/^P/ P/'
+ eval $restore_shopts
+ return 1
+ elif [[ "${arg[1]}" == "invalid" ]]; then
+ echo -ne "\n Invalid command: ${args[@]} [$arg]\n\n" >&2
+ eval $restore_shopts
+ return 1
+ fi
+
+ if [ -f "$tpath/$arg/node.def" ] ; then
+ tpath+=/$arg
+ elif [ -f $tpath/node.tag/node.def ] ; then
+ tpath+=/node.tag
+ else
+ echo -ne "\n Invalid command: ${args[@]} [$arg]\n\n" >&2
+ eval $restore_shopts
+ return 1
+ fi
+ if [[ "$arg" == "node.tag" ]]; then
+ args[$i]=$orig_arg
+ else
+ args[$i]=$arg
+ fi
+ let "i+=1"
+ done
+
+ local run_cmd=$(_vyatta_op_get_node_def_field $tpath/node.def run)
+ run_cmd=$(_vyatta_op_conv_run_cmd "$run_cmd") # convert the positional parameters
+ local ret=0
+ # Exception for the `show file` command
+ local file_cmd='\$\{vyos_op_scripts_dir\}\/file\.py'
+ local cmd_regex="^(LESSOPEN=|less|pager|tail|(sudo )?$file_cmd).*"
+ if [ -n "$run_cmd" ]; then
+ eval $restore_shopts
+ if [[ -t 1 && "${args[1]}" == "show" && ! $run_cmd =~ $cmd_regex ]] ; then
+ eval "($run_cmd) | ${VYATTA_PAGER:-cat}"
+ else
+ eval "$run_cmd"
+ fi
+ else
+ echo -ne "\n Incomplete command: ${args[@]}\n\n" >&2
+ eval $restore_shopts
+ ret=1
+ fi
+ return $ret
+}
+
+### Local Variables:
+### mode: shell-script
+### End:
diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv
new file mode 100644
index 000000000..1507f4f0d
--- /dev/null
+++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv
@@ -0,0 +1,97 @@
+#!/bin/bash
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# **** End License ****
+
+source /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common
+
+declare -a op_allowed
+declare -a toplevel
+
+op_allowed=( $(cat /opt/vyatta/etc/shell/level/users/allowed-op.in) )
+toplevel=( $(ls /opt/vyatta/share/vyatta-op/templates/) )
+
+vyatta_unpriv_ambiguous ()
+{
+ local -a filtered_cmds=()
+ get_prefix_filtered_list $1 op_allowed filtered_cmds
+ _vyatta_op_node_path=${vyatta_op_templates}
+ comps=$(_vyatta_op_help $1 ${filtered_cmds[@]})
+ echo -ne "\n Ambiguous command: [$1]\n"
+ echo -e "$comps\n" | sed -e 's/^P/ P/'
+}
+
+vyatta_unpriv_init ()
+{
+ # empty and default line compeletion
+ complete -E -F _vyatta_op_expand
+ complete -D -F _vyatta_op_default_expand
+
+ for cmd in "${op_allowed[@]}"; do
+ if is_elem_of ${cmd} toplevel; then
+ for pos in $(seq 1 ${#cmd}); do
+ case ${cmd:0:$pos} in
+ for|do|done|if|fi|case|while|tr )
+ continue ;;
+ *) ;;
+ esac
+ local -a filtered_cmds=()
+ get_prefix_filtered_list ${cmd:0:$pos} op_allowed filtered_cmds
+ local found
+ is_elem_of "${cmd:0:$pos}" op_allowed
+ found=$?
+ if [[ "${#filtered_cmds[@]}" == "1" || "${cmd:0:$pos}" == "$cmd" || "$found" == "0" ]]; then
+ local fcmd
+ if [[ "${#filtered_cmds[@]}" == "1" ]]; then
+ fcmd=${filtered_cmds[0]}
+ elif is_elem_of "${cmd:0:$pos}" op_allowed; then
+ fcmd=${cmd:0:$pos}
+ else
+ fcmd=$cmd
+ fi
+ eval alias ${cmd:0:$pos}=\'_vyatta_op_run $fcmd\'
+ else
+ eval alias ${cmd:0:$pos}=\'vyatta_unpriv_ambiguous ${cmd:0:$pos}\'
+ fi
+ complete -F _vyatta_op_expand ${cmd:0:$pos}
+ done
+ fi
+ done
+ if [[ "$VYATTA_USER_LEVEL_DIR" == "/opt/vyatta/etc/shell/level/users" ]]; then
+ PS1='\u@\h> '
+ fi
+}
+
+vyatta_unpriv_gen_allowed () {
+ local -a allowed_cmds=()
+ rm -rf /opt/vyatta/etc/shell/level/users/allowed-op
+ for cmd in "${op_allowed[@]}"; do
+ if is_elem_of ${cmd} toplevel; then
+ for pos in $(seq 1 ${#cmd}); do
+ case ${cmd:0:$pos} in
+ for|do|done|if|fi|case|while|tr )
+ continue ;;
+ *) ;;
+ esac
+ if ! is_elem_of ${cmd:0:$pos} allowed_cmds; then
+ allowed_cmds+=( ${cmd:0:$pos} )
+ echo ${cmd:0:$pos} >> /opt/vyatta/etc/shell/level/users/allowed-op
+ fi
+ done
+ else
+ echo ${cmd} >> /opt/vyatta/etc/shell/level/users/allowed-op
+ fi
+ done
+}
diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py
index 18d555f2d..800767219 100644
--- a/src/services/api/graphql/session/errors/op_mode_errors.py
+++ b/src/services/api/graphql/session/errors/op_mode_errors.py
@@ -1,5 +1,6 @@
op_mode_err_msg = {
"UnconfiguredSubsystem": "subsystem is not configured or not running",
+ "UnconfiguredObject": "object does not exist in the system configuration",
"DataUnavailable": "data currently unavailable",
"PermissionDenied": "client does not have permission",
"InsufficientResources": "insufficient system resources",
@@ -9,6 +10,7 @@ op_mode_err_msg = {
op_mode_err_code = {
"UnconfiguredSubsystem": 2000,
+ "UnconfiguredObject": 2003,
"DataUnavailable": 2001,
"InsufficientResources": 2002,
"PermissionDenied": 1003,
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index d92b539c8..a4b839a7f 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -30,7 +30,6 @@ from vyos.defaults import directories
from vyos.utils.boot import boot_configuration_complete
from vyos.configsource import ConfigSourceString
from vyos.configsource import ConfigSourceError
-from vyos.configdep import call_dependents
from vyos.config import Config
from vyos import ConfigError
@@ -134,7 +133,8 @@ def explicit_print(path, mode, msg):
except OSError:
logger.critical("error explicit_print")
-def run_script(script, config, args) -> int:
+def run_script(script_name, config, args) -> int:
+ script = conf_mode_scripts[script_name]
script.argv = args
config.set_level([])
try:
@@ -143,7 +143,7 @@ def run_script(script, config, args) -> int:
script.generate(c)
script.apply(c)
except ConfigError as e:
- logger.critical(e)
+ logger.error(e)
explicit_print(session_out, session_mode, str(e))
return R_ERROR_COMMIT
except Exception as e:
@@ -219,6 +219,7 @@ def process_node_data(config, data, last: bool = False) -> int:
script_name = None
args = []
+ config.dependency_list.clear()
res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data)
if res.group(1):
@@ -234,17 +235,10 @@ def process_node_data(config, data, last: bool = False) -> int:
args.insert(0, f'{script_name}.py')
if script_name not in include_set:
- # call dependents now if last element of prio queue is run
- # independent of configd
- if last:
- call_dependents(dependent_func=config.dependent_func)
return R_PASS
with stdout_redirected(session_out, session_mode):
- result = run_script(conf_mode_scripts[script_name], config, args)
-
- if last and result == R_SUCCESS:
- call_dependents(dependent_func=config.dependent_func)
+ result = run_script(script_name, config, args)
return result
diff --git a/src/services/vyos-conntrack-logger b/src/services/vyos-conntrack-logger
new file mode 100755
index 000000000..9c31b465f
--- /dev/null
+++ b/src/services/vyos-conntrack-logger
@@ -0,0 +1,458 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import grp
+import logging
+import multiprocessing
+import os
+import queue
+import signal
+import socket
+import threading
+from datetime import timedelta
+from pathlib import Path
+from time import sleep
+from typing import Dict, AnyStr
+
+from pyroute2 import conntrack
+from pyroute2.netlink import nfnetlink
+from pyroute2.netlink.nfnetlink import NFNL_SUBSYS_CTNETLINK
+from pyroute2.netlink.nfnetlink.nfctsocket import nfct_msg, \
+ IPCTNL_MSG_CT_DELETE, IPCTNL_MSG_CT_NEW, IPS_SEEN_REPLY, \
+ IPS_OFFLOAD, IPS_ASSURED
+
+from vyos.utils.file import read_json
+
+
+shutdown_event = multiprocessing.Event()
+
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+logger = logging.getLogger(__name__)
+
+
+class DebugFormatter(logging.Formatter):
+ def format(self, record):
+ self._style._fmt = '[%(asctime)s] %(levelname)s: %(message)s'
+ return super().format(record)
+
+
+def set_log_level(level: str) -> None:
+ if level == 'debug':
+ logger.setLevel(logging.DEBUG)
+ logger.parent.handlers[0].setFormatter(DebugFormatter())
+ else:
+ logger.setLevel(logging.INFO)
+
+
+EVENT_NAME_TO_GROUP = {
+ 'new': nfnetlink.NFNLGRP_CONNTRACK_NEW,
+ 'update': nfnetlink.NFNLGRP_CONNTRACK_UPDATE,
+ 'destroy': nfnetlink.NFNLGRP_CONNTRACK_DESTROY
+}
+
+# https://github.com/torvalds/linux/blob/1dfe225e9af5bd3399a1dbc6a4df6a6041ff9c23/include/uapi/linux/netfilter/nf_conntrack_tcp.h#L9
+TCP_CONNTRACK_SYN_SENT = 1
+TCP_CONNTRACK_SYN_RECV = 2
+TCP_CONNTRACK_ESTABLISHED = 3
+TCP_CONNTRACK_FIN_WAIT = 4
+TCP_CONNTRACK_CLOSE_WAIT = 5
+TCP_CONNTRACK_LAST_ACK = 6
+TCP_CONNTRACK_TIME_WAIT = 7
+TCP_CONNTRACK_CLOSE = 8
+TCP_CONNTRACK_LISTEN = 9
+TCP_CONNTRACK_MAX = 10
+TCP_CONNTRACK_IGNORE = 11
+TCP_CONNTRACK_RETRANS = 12
+TCP_CONNTRACK_UNACK = 13
+TCP_CONNTRACK_TIMEOUT_MAX = 14
+
+TCP_CONNTRACK_TO_NAME = {
+ TCP_CONNTRACK_SYN_SENT: "SYN_SENT",
+ TCP_CONNTRACK_SYN_RECV: "SYN_RECV",
+ TCP_CONNTRACK_ESTABLISHED: "ESTABLISHED",
+ TCP_CONNTRACK_FIN_WAIT: "FIN_WAIT",
+ TCP_CONNTRACK_CLOSE_WAIT: "CLOSE_WAIT",
+ TCP_CONNTRACK_LAST_ACK: "LAST_ACK",
+ TCP_CONNTRACK_TIME_WAIT: "TIME_WAIT",
+ TCP_CONNTRACK_CLOSE: "CLOSE",
+ TCP_CONNTRACK_LISTEN: "LISTEN",
+ TCP_CONNTRACK_MAX: "MAX",
+ TCP_CONNTRACK_IGNORE: "IGNORE",
+ TCP_CONNTRACK_RETRANS: "RETRANS",
+ TCP_CONNTRACK_UNACK: "UNACK",
+ TCP_CONNTRACK_TIMEOUT_MAX: "TIMEOUT_MAX",
+}
+
+# https://github.com/torvalds/linux/blob/1dfe225e9af5bd3399a1dbc6a4df6a6041ff9c23/include/uapi/linux/netfilter/nf_conntrack_sctp.h#L8
+SCTP_CONNTRACK_CLOSED = 1
+SCTP_CONNTRACK_COOKIE_WAIT = 2
+SCTP_CONNTRACK_COOKIE_ECHOED = 3
+SCTP_CONNTRACK_ESTABLISHED = 4
+SCTP_CONNTRACK_SHUTDOWN_SENT = 5
+SCTP_CONNTRACK_SHUTDOWN_RECD = 6
+SCTP_CONNTRACK_SHUTDOWN_ACK_SENT = 7
+SCTP_CONNTRACK_HEARTBEAT_SENT = 8
+SCTP_CONNTRACK_HEARTBEAT_ACKED = 9 # no longer used
+SCTP_CONNTRACK_MAX = 10
+
+SCTP_CONNTRACK_TO_NAME = {
+ SCTP_CONNTRACK_CLOSED: 'CLOSED',
+ SCTP_CONNTRACK_COOKIE_WAIT: 'COOKIE_WAIT',
+ SCTP_CONNTRACK_COOKIE_ECHOED: 'COOKIE_ECHOED',
+ SCTP_CONNTRACK_ESTABLISHED: 'ESTABLISHED',
+ SCTP_CONNTRACK_SHUTDOWN_SENT: 'SHUTDOWN_SENT',
+ SCTP_CONNTRACK_SHUTDOWN_RECD: 'SHUTDOWN_RECD',
+ SCTP_CONNTRACK_SHUTDOWN_ACK_SENT: 'SHUTDOWN_ACK_SENT',
+ SCTP_CONNTRACK_HEARTBEAT_SENT: 'HEARTBEAT_SENT',
+ SCTP_CONNTRACK_HEARTBEAT_ACKED: 'HEARTBEAT_ACKED',
+ SCTP_CONNTRACK_MAX: 'MAX',
+}
+
+PROTO_CONNTRACK_TO_NAME = {
+ 'TCP': TCP_CONNTRACK_TO_NAME,
+ 'SCTP': SCTP_CONNTRACK_TO_NAME
+}
+
+SUPPORTED_PROTO_TO_NAME = {
+ socket.IPPROTO_ICMP: 'icmp',
+ socket.IPPROTO_TCP: 'tcp',
+ socket.IPPROTO_UDP: 'udp',
+}
+
+PROTO_TO_NAME = {
+ socket.IPPROTO_ICMPV6: 'icmpv6',
+ socket.IPPROTO_SCTP: 'sctp',
+ socket.IPPROTO_GRE: 'gre',
+}
+
+PROTO_TO_NAME.update(SUPPORTED_PROTO_TO_NAME)
+
+
+def sig_handler(signum, frame):
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}]: {"Shutdown" if signum == signal.SIGTERM else "Reload"} signal received...')
+ shutdown_event.set()
+
+
+def format_flow_data(data: Dict) -> AnyStr:
+ """
+ Formats the flow event data into a string suitable for logging.
+ """
+ key_format = {
+ 'SRC_PORT': 'sport',
+ 'DST_PORT': 'dport'
+ }
+ message = f"src={data['ADDR'].get('SRC')} dst={data['ADDR'].get('DST')}"
+
+ for key in ['SRC_PORT', 'DST_PORT', 'TYPE', 'CODE', 'ID']:
+ tmp = data['PROTO'].get(key)
+ if tmp is not None:
+ key = key_format.get(key, key)
+ message += f" {key.lower()}={tmp}"
+
+ if 'COUNTERS' in data:
+ for key in ['PACKETS', 'BYTES']:
+ tmp = data['COUNTERS'].get(key)
+ if tmp is not None:
+ message += f" {key.lower()}={tmp}"
+
+ return message
+
+
+def format_event_message(event: Dict) -> AnyStr:
+ """
+ Formats the internal parsed event data into a string suitable for logging.
+ """
+ event_type = f"[{event['COMMON']['EVENT_TYPE'].upper()}]"
+ message = f"{event_type:<{9}} {event['COMMON']['ID']} " \
+ f"{event['ORIG']['PROTO'].get('NAME'):<{8}} " \
+ f"{event['ORIG']['PROTO'].get('NUMBER')} "
+
+ tmp = event['COMMON']['TIME_OUT']
+ if tmp is not None: message += f"{tmp} "
+
+ if proto_info := event['COMMON'].get('PROTO_INFO'):
+ message += f"{proto_info.get('STATE_NAME')} "
+
+ for key in ['ORIG', 'REPLY']:
+ message += f"{format_flow_data(event[key])} "
+ if key == 'ORIG' and not (event['COMMON']['STATUS'] & IPS_SEEN_REPLY):
+ message += f"[UNREPLIED] "
+
+ tmp = event['COMMON']['MARK']
+ if tmp is not None: message += f"mark={tmp} "
+
+ if event['COMMON']['STATUS'] & IPS_OFFLOAD: message += f" [OFFLOAD] "
+ elif event['COMMON']['STATUS'] & IPS_ASSURED: message += f" [ASSURED] "
+
+ if tmp := event['COMMON']['PORTID']: message += f"portid={tmp} "
+ if tstamp := event['COMMON'].get('TIMESTAMP'):
+ message += f"start={tstamp['START']} stop={tstamp['STOP']} "
+ delta_ns = tstamp['STOP'] - tstamp['START']
+ delta_s = delta_ns // 1e9
+ remaining_ns = delta_ns % 1e9
+ delta = timedelta(seconds=delta_s, microseconds=remaining_ns / 1000)
+ message += f"delta={delta.total_seconds()} "
+
+ return message
+
+
+def parse_event_type(header: Dict) -> AnyStr:
+ """
+ Extract event type from nfct_msg. new, update, destroy
+ """
+ event_type = 'unknown'
+ if header['type'] == IPCTNL_MSG_CT_DELETE | (NFNL_SUBSYS_CTNETLINK << 8):
+ event_type = 'destroy'
+ elif header['type'] == IPCTNL_MSG_CT_NEW | (NFNL_SUBSYS_CTNETLINK << 8):
+ event_type = 'update'
+ if header['flags']:
+ event_type = 'new'
+ return event_type
+
+
+def parse_proto(cta: nfct_msg.cta_tuple) -> Dict:
+ """
+ Extract proto info from nfct_msg. src/dst port, code, type, id
+ """
+ data = dict()
+
+ cta_proto = cta.get_attr('CTA_TUPLE_PROTO')
+ proto_num = cta_proto.get_attr('CTA_PROTO_NUM')
+
+ data['NUMBER'] = proto_num
+ data['NAME'] = PROTO_TO_NAME.get(proto_num, 'unknown')
+
+ if proto_num in (socket.IPPROTO_ICMP, socket.IPPROTO_ICMPV6):
+ pref = 'CTA_PROTO_ICMP'
+ if proto_num == socket.IPPROTO_ICMPV6: pref += 'V6'
+ keys = ['TYPE', 'CODE', 'ID']
+ else:
+ pref = 'CTA_PROTO'
+ keys = ['SRC_PORT', 'DST_PORT']
+
+ for key in keys:
+ data[key] = cta_proto.get_attr(f'{pref}_{key}')
+
+ return data
+
+
+def parse_proto_info(cta: nfct_msg.cta_protoinfo) -> Dict:
+ """
+ Extract proto state and state name from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+
+ for proto in ['TCP', 'SCTP']:
+ if proto_info := cta.get_attr(f'CTA_PROTOINFO_{proto}'):
+ data['STATE'] = proto_info.get_attr(f'CTA_PROTOINFO_{proto}_STATE')
+ data['STATE_NAME'] = PROTO_CONNTRACK_TO_NAME.get(proto, {}).get(data['STATE'], 'unknown')
+ return data
+
+
+def parse_timestamp(cta: nfct_msg.cta_timestamp) -> Dict:
+ """
+ Extract timestamp from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+ data['START'] = cta.get_attr('CTA_TIMESTAMP_START')
+ data['STOP'] = cta.get_attr('CTA_TIMESTAMP_STOP')
+
+ return data
+
+
+def parse_ip_addr(family: int, cta: nfct_msg.cta_tuple) -> Dict:
+ """
+ Extract ip adr from nfct_msg
+ """
+ data = dict()
+ cta_ip = cta.get_attr('CTA_TUPLE_IP')
+
+ if family == socket.AF_INET:
+ pref = 'CTA_IP_V4'
+ elif family == socket.AF_INET6:
+ pref = 'CTA_IP_V6'
+ else:
+ logger.error(f'Undefined INET: {family}')
+ raise NotImplementedError(family)
+
+ for direct in ['SRC', 'DST']:
+ data[direct] = cta_ip.get_attr(f'{pref}_{direct}')
+
+ return data
+
+
+def parse_counters(cta: nfct_msg.cta_counters) -> Dict:
+ """
+ Extract counters from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+
+ for key in ['PACKETS', 'BYTES']:
+ tmp = cta.get_attr(f'CTA_COUNTERS_{key}')
+ if tmp is None:
+ tmp = cta.get_attr(f'CTA_COUNTERS32_{key}')
+ data['key'] = tmp
+
+ return data
+
+
+def is_need_to_log(event_type: AnyStr, proto_num: int, conf_event: Dict):
+ """
+ Filter message by event type and protocols
+ """
+ conf = conf_event.get(event_type)
+ if conf == {} or conf.get(SUPPORTED_PROTO_TO_NAME.get(proto_num, 'other')) is not None:
+ return True
+ return False
+
+
+def parse_conntrack_event(msg: nfct_msg, conf_event: Dict) -> Dict:
+ """
+ Convert nfct_msg to internal data dict.
+ """
+ data = dict()
+ event_type = parse_event_type(msg['header'])
+ proto_num = msg.get_nested('CTA_TUPLE_ORIG', 'CTA_TUPLE_PROTO', 'CTA_PROTO_NUM')
+
+ if not is_need_to_log(event_type, proto_num, conf_event):
+ return data
+
+ data = {
+ 'COMMON': {
+ 'ID': msg.get_attr('CTA_ID'),
+ 'EVENT_TYPE': event_type,
+ 'TIME_OUT': msg.get_attr('CTA_TIMEOUT'),
+ 'MARK': msg.get_attr('CTA_MARK'),
+ 'PORTID': msg['header'].get('pid'),
+ 'PROTO_INFO': parse_proto_info(msg.get_attr('CTA_PROTOINFO')),
+ 'STATUS': msg.get_attr('CTA_STATUS'),
+ 'TIMESTAMP': parse_timestamp(msg.get_attr('CTA_TIMESTAMP'))
+ },
+ 'ORIG': {},
+ 'REPLY': {},
+ }
+
+ for direct in ['ORIG', 'REPLY']:
+ data[direct]['ADDR'] = parse_ip_addr(msg['nfgen_family'], msg.get_attr(f'CTA_TUPLE_{direct}'))
+ data[direct]['PROTO'] = parse_proto(msg.get_attr(f'CTA_TUPLE_{direct}'))
+ data[direct]['COUNTERS'] = parse_counters(msg.get_attr(f'CTA_COUNTERS_{direct}'))
+
+ return data
+
+
+def worker(ct: conntrack.Conntrack, shutdown_event: multiprocessing.Event, conf_event: Dict):
+ """
+ Main function of parser worker process
+ """
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}] started')
+ timeout = 0.1
+ while not shutdown_event.is_set():
+ if not ct.buffer_queue.empty():
+ try:
+ for msg in ct.get():
+ parsed_event = parse_conntrack_event(msg, conf_event)
+ if parsed_event:
+ message = format_event_message(parsed_event)
+ if logger.level == logging.DEBUG:
+ logger.debug(f"[{process_name}]: {message} raw: {msg}")
+ else:
+ logger.info(message)
+ except queue.Full:
+ logger.error("Conntrack message queue if full.")
+ except Exception as e:
+ logger.error(f"Error in queue: {e.__class__} {e}")
+ else:
+ sleep(timeout)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to vyos-conntrack-logger configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config = read_json(args.config)
+ except Exception as err:
+ logger.error(f'Configuration file "{args.config}" does not exist or malformed: {err}')
+ exit(1)
+
+ set_log_level(config.get('log_level', 'info'))
+
+ signal.signal(signal.SIGHUP, sig_handler)
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ if 'event' in config:
+ event_groups = list(config.get('event').keys())
+ else:
+ logger.error(f'Configuration is wrong. Event filter is empty.')
+ exit(1)
+
+ conf_event = config['event']
+ qsize = config.get('queue_size')
+ ct = conntrack.Conntrack(async_qsize=int(qsize) if qsize else None)
+ ct.buffer_queue = multiprocessing.Queue(ct.async_qsize)
+ ct.bind(async_cache=True)
+
+ for name in event_groups:
+ if group := EVENT_NAME_TO_GROUP.get(name):
+ ct.add_membership(group)
+ else:
+ logger.error(f'Unexpected event group {name}')
+ processes = list()
+ try:
+ for _ in range(multiprocessing.cpu_count()):
+ p = multiprocessing.Process(target=worker, args=(ct,
+ shutdown_event,
+ conf_event))
+ processes.append(p)
+ p.start()
+ logger.info('Conntrack socket bound and listening for messages.')
+
+ while not shutdown_event.is_set():
+ if not ct.pthread.is_alive():
+ if ct.buffer_queue.qsize()/ct.async_qsize < 0.9:
+ if not shutdown_event.is_set():
+ logger.debug('Restart listener thread')
+ # restart listener thread after queue overloaded when queue size low than 90%
+ ct.pthread = threading.Thread(
+ name="Netlink async cache", target=ct.async_recv
+ )
+ ct.pthread.daemon = True
+ ct.pthread.start()
+ else:
+ sleep(0.1)
+ finally:
+ for p in processes:
+ p.join()
+ if not p.is_alive():
+ logger.debug(f"[{p.name}]: finished")
+ ct.close()
+ logging.info("Conntrack socket closed.")
+ exit()
diff --git a/src/systemd/vyos-conntrack-logger.service b/src/systemd/vyos-conntrack-logger.service
new file mode 100644
index 000000000..9bc1d857b
--- /dev/null
+++ b/src/systemd/vyos-conntrack-logger.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=VyOS conntrack logger daemon
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=conntrackd.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-conntrack-logger -c /run/vyos-conntrack-logger.conf
+Type=idle
+
+SyslogIdentifier=vyos-conntrack-logger
+SyslogFacility=daemon
+
+Restart=on-failure
+
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=multi-user.target