summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/interfaces_wireless.py10
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py16
-rwxr-xr-xsrc/conf_mode/protocols_pim.py14
-rwxr-xr-xsrc/conf_mode/qos.py23
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py2
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py2
-rwxr-xr-xsrc/conf_mode/system_host-name.py10
-rwxr-xr-xsrc/helpers/vyos_config_sync.py23
-rwxr-xr-xsrc/migration-scripts/openconnect/2-to-350
-rwxr-xr-xsrc/op_mode/firewall.py78
-rwxr-xr-xsrc/services/vyos-configd2
11 files changed, 202 insertions, 28 deletions
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
index 02b4a2500..c0a17c0bc 100755
--- a/src/conf_mode/interfaces_wireless.py
+++ b/src/conf_mode/interfaces_wireless.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-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
@@ -31,8 +31,9 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import WiFiIf
from vyos.template import render
-from vyos.utils.process import call
from vyos.utils.dict import dict_search
+from vyos.utils.kernel import check_kmod
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -118,6 +119,10 @@ def verify(wifi):
if 'physical_device' not in wifi:
raise ConfigError('You must specify a physical-device "phy"')
+ physical_device = wifi['physical_device']
+ if not os.path.exists(f'/sys/class/ieee80211/{physical_device}'):
+ raise ConfigError(f'Wirelss interface PHY "{physical_device}" does not exist!')
+
if 'type' not in wifi:
raise ConfigError('You must specify a WiFi mode')
@@ -266,6 +271,7 @@ def apply(wifi):
if __name__ == '__main__':
try:
+ check_kmod('mac80211')
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 2b16de775..4df97d133 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -473,6 +473,22 @@ def verify(bgp):
if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
raise ConfigError('route-reflector-client only supported for iBGP peers')
+ # T5833 not all AFIs are supported for VRF
+ if 'vrf' in bgp and 'address_family' in peer_config:
+ unsupported_vrf_afi = {
+ 'ipv4_flowspec',
+ 'ipv6_flowspec',
+ 'ipv4_labeled_unicast',
+ 'ipv6_labeled_unicast',
+ 'ipv4_vpn',
+ 'ipv6_vpn',
+ }
+ for afi in peer_config['address_family']:
+ if afi in unsupported_vrf_afi:
+ raise ConfigError(
+ f"VRF is not allowed for address-family '{afi.replace('_', '-')}'"
+ )
+
# Throw an error if a peer group is not configured for allow range
for prefix in dict_search('listen.range', bgp) or []:
# we can not use dict_search() here as prefix contains dots ...
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index 09c3be8df..d450d11ca 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright (C) 2020-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
@@ -16,6 +16,7 @@
import os
+from ipaddress import IPv4Address
from ipaddress import IPv4Network
from signal import SIGTERM
from sys import exit
@@ -32,6 +33,9 @@ from vyos import frr
from vyos import airbag
airbag.enable()
+RESERVED_MC_NET = '224.0.0.0/24'
+
+
def get_config(config=None):
if config:
conf = config
@@ -92,9 +96,15 @@ def verify(pim):
if 'interface' not in pim:
raise ConfigError('PIM require defined interfaces!')
- for interface in pim['interface']:
+ for interface, interface_config in pim['interface'].items():
verify_interface_exists(interface)
+ # Check join group in reserved net
+ if 'igmp' in interface_config and 'join' in interface_config['igmp']:
+ for join_addr in interface_config['igmp']['join']:
+ if IPv4Address(join_addr) in IPv4Network(RESERVED_MC_NET):
+ raise ConfigError(f'Groups within {RESERVED_MC_NET} are reserved and cannot be joined!')
+
if 'rp' in pim:
if 'address' not in pim['rp']:
raise ConfigError('PIM rendezvous point needs to be defined!')
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
index 3dfb4bab8..ccfc8f6b8 100755
--- a/src/conf_mode/qos.py
+++ b/src/conf_mode/qos.py
@@ -70,6 +70,22 @@ def get_shaper(qos, interface_config, direction):
return (map_vyops_tc[shaper_type], shaper_config)
+
+def _clean_conf_dict(conf):
+ """
+ Delete empty nodes from config e.g.
+ match ADDRESS30 {
+ ip {
+ source {}
+ }
+ }
+ """
+ if isinstance(conf, dict):
+ return {node: _clean_conf_dict(val) for node, val in conf.items() if val != {} and _clean_conf_dict(val) != {}}
+ else:
+ return conf
+
+
def get_config(config=None):
if config:
conf = config
@@ -120,6 +136,13 @@ def get_config(config=None):
if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]:
qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \
str(int(4 * max_thr))
+ # cleanup empty match config
+ if 'class' in p_config:
+ for cls, cls_config in p_config['class'].items():
+ if 'match' in cls_config:
+ cls_config['match'] = _clean_conf_dict(cls_config['match'])
+ if cls_config['match'] == {}:
+ del cls_config['match']
return qos
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 11e950782..28b7fb03c 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -66,7 +66,7 @@ def verify(ipoe):
raise ConfigError('No IPoE interface configured')
for interface, iface_config in ipoe['interface'].items():
- verify_interface_exists(interface)
+ verify_interface_exists(interface, warning_only=True)
if 'client_subnet' in iface_config and 'vlan' in iface_config:
raise ConfigError('Option "client-subnet" and "vlan" are mutually exclusive, '
'use "client-ip-pool" instead!')
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index c7bdc52f0..c95f976d3 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -122,7 +122,7 @@ def verify(pppoe):
# Check is interface exists in the system
for interface in pppoe['interface']:
- verify_interface_exists(interface)
+ verify_interface_exists(interface, warning_only=True)
return None
diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py
index 8975cadb6..3f245f166 100755
--- a/src/conf_mode/system_host-name.py
+++ b/src/conf_mode/system_host-name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -22,6 +22,7 @@ import vyos.hostsd_client
from vyos.base import Warning
from vyos.config import Config
+from vyos.configdict import leaf_node_changed
from vyos.ifconfig import Section
from vyos.template import is_ip
from vyos.utils.process import cmd
@@ -37,6 +38,7 @@ default_config_data = {
'domain_search': [],
'nameserver': [],
'nameservers_dhcp_interfaces': {},
+ 'snmpd_restart_reqired': False,
'static_host_mapping': {}
}
@@ -52,6 +54,10 @@ def get_config(config=None):
hosts['hostname'] = conf.return_value(['system', 'host-name'])
+ base = ['system']
+ if leaf_node_changed(conf, base + ['host-name']) or leaf_node_changed(conf, base + ['domain-name']):
+ hosts['snmpd_restart_reqired'] = True
+
# This may happen if the config is not loaded yet,
# e.g. if run by cloud-init
if not hosts['hostname']:
@@ -171,7 +177,7 @@ def apply(config):
call("systemctl restart rsyslog.service")
# If SNMP is running, restart it too
- if process_named_running('snmpd'):
+ if process_named_running('snmpd') and config['snmpd_restart_reqired']:
call('systemctl restart snmpd.service')
return None
diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py
index 0604b2837..9d9aec376 100755
--- a/src/helpers/vyos_config_sync.py
+++ b/src/helpers/vyos_config_sync.py
@@ -93,7 +93,8 @@ def set_remote_config(
key: str,
op: str,
mask: Dict[str, Any],
- config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+ config: Dict[str, Any],
+ port: int) -> Optional[Dict[str, Any]]:
"""Loads the VyOS configuration in JSON format to a remote host.
Args:
@@ -102,6 +103,7 @@ def set_remote_config(
op (str): The operation to perform (set or load).
mask (dict): The dict of paths in sections.
config (dict): The dict of masked config data.
+ port (int): The remote API port
Returns:
Optional[Dict[str, Any]]: The response from the remote host as a
@@ -113,7 +115,7 @@ def set_remote_config(
# Disable the InsecureRequestWarning
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
- url = f'https://{address}/configure-section'
+ url = f'https://{address}:{port}/configure-section'
data = json.dumps({
'op': op,
'mask': mask,
@@ -138,7 +140,8 @@ def is_section_revised(section: List[str]) -> bool:
def config_sync(secondary_address: str,
secondary_key: str,
sections: List[list[str]],
- mode: str):
+ mode: str,
+ secondary_port: int):
"""Retrieve a config section from primary router in JSON format and send it to
secondary router
"""
@@ -158,7 +161,8 @@ def config_sync(secondary_address: str,
key=secondary_key,
op=mode,
mask=mask_dict,
- config=config_dict)
+ config=config_dict,
+ port=secondary_port)
logger.debug(f"Set config for sections '{sections}': {set_config}")
@@ -178,14 +182,12 @@ if __name__ == '__main__':
secondary_address = config.get('secondary', {}).get('address')
secondary_address = bracketize_ipv6(secondary_address)
secondary_key = config.get('secondary', {}).get('key')
+ secondary_port = int(config.get('secondary', {}).get('port', 443))
sections = config.get('section')
timeout = int(config.get('secondary', {}).get('timeout'))
- if not all([
- mode, secondary_address, secondary_key, sections
- ]):
- logger.error(
- "Missing required configuration data for config synchronization.")
+ if not all([mode, secondary_address, secondary_key, sections]):
+ logger.error("Missing required configuration data for config synchronization.")
exit(0)
# Generate list_sections of sections/subsections
@@ -200,5 +202,4 @@ if __name__ == '__main__':
else:
list_sections.append([section])
- config_sync(secondary_address, secondary_key,
- list_sections, mode)
+ config_sync(secondary_address, secondary_key, list_sections, mode, secondary_port)
diff --git a/src/migration-scripts/openconnect/2-to-3 b/src/migration-scripts/openconnect/2-to-3
new file mode 100755
index 000000000..e78fc8a91
--- /dev/null
+++ b/src/migration-scripts/openconnect/2-to-3
@@ -0,0 +1,50 @@
+#!/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/>.
+
+# T4982: Retain prior default TLS version (v1.0) when upgrading installations with existing openconnect configurations
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+
+config = ConfigTree(config_file)
+cfg_base = ['vpn', 'openconnect']
+
+# bail out early if service is unconfigured
+if not config.exists(cfg_base):
+ sys.exit(0)
+
+# new default is TLS 1.2 - set explicit old default value of TLS 1.0 for upgraded configurations to keep compatibility
+tls_min_path = cfg_base + ['tls-version-min']
+if not config.exists(tls_min_path):
+ config.set(tls_min_path, value='1.0')
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 25554b781..442c186cc 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -16,6 +16,7 @@
import argparse
import ipaddress
+import json
import re
import tabulate
import textwrap
@@ -89,10 +90,38 @@ def get_nftables_details(family, hook, priority):
out[rule_id] = rule
return out
-def output_firewall_vertical(rules, headers):
+def get_nftables_group_members(family, table, name):
+ prefix = 'ip6' if family == 'ipv6' else 'ip'
+ out = []
+
+ try:
+ results_str = cmd(f'sudo nft -j list set {prefix} {table} {name}')
+ results = json.loads(results_str)
+ except:
+ return out
+
+ if 'nftables' not in results:
+ return out
+
+ for obj in results['nftables']:
+ if 'set' not in obj:
+ continue
+
+ set_obj = obj['set']
+
+ if 'elem' in set_obj:
+ for elem in set_obj['elem']:
+ if isinstance(elem, str):
+ out.append(elem)
+ elif isinstance(elem, dict) and 'elem' in elem:
+ out.append(elem['elem'])
+
+ return out
+
+def output_firewall_vertical(rules, headers, adjust=True):
for rule in rules:
- adjusted_rule = rule + [""] * (len(headers) - len(rule)) # account for different header length, like default-action
- transformed_rule = [[header, textwrap.fill(adjusted_rule[i].replace('\n', ' '), 65)] for i, header in enumerate(headers)] # create key-pair list from headers and rules lists; wrap at 100 char
+ adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action
+ transformed_rule = [[header, textwrap.fill(adjusted_rule[i].replace('\n', ' '), 65)] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char
print(tabulate.tabulate(transformed_rule, tablefmt="presto"))
print()
@@ -453,6 +482,7 @@ def show_firewall_group(name=None):
return out
rows = []
+ header_tail = []
for group_type, group_type_conf in firewall['group'].items():
##
@@ -479,21 +509,53 @@ def show_firewall_group(name=None):
rows.append(row)
else:
+ if not args.detail:
+ header_tail = ['Timeout', 'Expires']
+
for dynamic_type in ['address_group', 'ipv6_address_group']:
+ family = 'ipv4' if dynamic_type == 'address_group' else 'ipv6'
+ prefix = 'DA_' if dynamic_type == 'address_group' else 'DA6_'
if dynamic_type in firewall['group']['dynamic_group']:
for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items():
references = find_references(dynamic_type, dynamic_name)
row = [dynamic_name, textwrap.fill(dynamic_conf.get('description') or '', 50), dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D']
- row.append('N/D')
- rows.append(row)
+
+ members = get_nftables_group_members(family, 'vyos_filter', f'{prefix}{dynamic_name}')
+
+ if not members:
+ if args.detail:
+ row.append('N/D')
+ else:
+ row += ["N/D"] * 3
+ rows.append(row)
+ continue
+
+ for idx, member in enumerate(members):
+ val = member.get('val', 'N/D')
+ timeout = str(member.get('timeout', 'N/D'))
+ expires = str(member.get('expires', 'N/D'))
+
+ if args.detail:
+ row.append(f'{val} (timeout: {timeout}, expires: {expires})')
+ continue
+
+ if idx > 0:
+ row = [""] * 4
+
+ row += [val, timeout, expires]
+ rows.append(row)
+
+ if args.detail:
+ header_tail += [""] * (len(members) - 1)
+ rows.append(row)
if rows:
print('Firewall Groups\n')
if args.detail:
- header = ['Name', 'Description','Type', 'References', 'Members']
- output_firewall_vertical(rows, header)
+ header = ['Name', 'Description', 'Type', 'References', 'Members'] + header_tail
+ output_firewall_vertical(rows, header, adjust=False)
else:
- header = ['Name', 'Type', 'References', 'Members']
+ header = ['Name', 'Type', 'References', 'Members'] + header_tail
for i in rows:
rows[rows.index(i)].pop(1)
print(tabulate.tabulate(rows, header))
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 648a017d5..c89c486e5 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -236,7 +236,7 @@ def process_node_data(config, data, last: bool = False) -> int:
with stdout_redirected(session_out, session_mode):
result = run_script(conf_mode_scripts[script_name], config, args)
- if last:
+ if last and result == R_SUCCESS:
call_dependents(dependent_func=config.dependent_func)
return result