summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/firewall.py163
-rwxr-xr-xsrc/conf_mode/interfaces_l2tpv3.py3
-rwxr-xr-xsrc/conf_mode/interfaces_wireguard.py6
-rwxr-xr-xsrc/conf_mode/interfaces_wireless.py3
-rwxr-xr-xsrc/conf_mode/nat.py3
-rwxr-xr-xsrc/conf_mode/nat64.py18
-rwxr-xr-xsrc/conf_mode/nat66.py3
-rwxr-xr-xsrc/conf_mode/policy_route.py29
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py31
-rwxr-xr-xsrc/conf_mode/system_acceleration.py3
-rwxr-xr-xsrc/conf_mode/system_console.py14
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py129
-rwxr-xr-xsrc/conf_mode/vrf.py17
13 files changed, 308 insertions, 114 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 352d5cbb1..02bf00bcc 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -36,6 +36,7 @@ from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
from vyos import ConfigError
from vyos import airbag
+from subprocess import run as subp_run
airbag.enable()
@@ -47,7 +48,12 @@ valid_groups = [
'domain_group',
'network_group',
'port_group',
- 'interface_group'
+ 'interface_group',
+ ## Added for group ussage in bridge firewall
+ 'ipv4_address_group',
+ 'ipv6_address_group',
+ 'ipv4_network_group',
+ 'ipv6_network_group'
]
nested_group_types = [
@@ -128,41 +134,32 @@ def get_config(config=None):
return firewall
-def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False):
+def verify_jump_target(firewall, hook, jump_target, family, 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')
+ if 'name' not in firewall[family]:
+ raise ConfigError(f'Invalid jump-target. Firewall {family} name {target} does not exist on the system')
+ elif target not in dict_search_args(firewall, family, 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall {family} name {target} does not exist on the system')
- no_ipsec_in = root_chain in ('output', )
+ target_rules = dict_search_args(firewall, family, 'name', target, 'rule')
+ no_ipsec_in = hook 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')
+ raise ConfigError(f'Invalid jump-target for {hook}. Firewall {family} 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}')
+ raise ConfigError(f'Loop detected in jump-targets, firewall {family} name {target} refers to previously traversed {family} name {child_target}')
targets_pending.append(child_target)
if len(targets_seen) == 7:
path_txt = ' -> '.join(targets_seen)
@@ -170,7 +167,7 @@ def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False)
targets_seen.append(target)
-def verify_rule(firewall, chain_name, rule_conf, ipv6):
+def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if 'action' not in rule_conf:
raise ConfigError('Rule action must be defined')
@@ -181,10 +178,10 @@ def verify_rule(firewall, chain_name, 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 chain_name != 'name': # This is a bit clumsy, but consolidates a chunk of code.
- verify_jump_target(firewall, chain_name, target, ipv6, recursive=True)
+ if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code.
+ verify_jump_target(firewall, hook, target, family, recursive=True)
else:
- verify_jump_target(firewall, chain_name, target, ipv6, recursive=False)
+ verify_jump_target(firewall, hook, target, family, recursive=False)
if rule_conf['action'] == 'offload':
if 'offload_target' not in rule_conf:
@@ -246,9 +243,9 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
raise ConfigError(f'Cannot match a tcp flag as set and not set')
if 'protocol' in rule_conf:
- if rule_conf['protocol'] == 'icmp' and ipv6:
+ if rule_conf['protocol'] == 'icmp' and family == 'ipv6':
raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp')
- if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6:
+ if rule_conf['protocol'] == 'ipv6-icmp' and family == 'ipv4':
raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp')
for side in ['destination', 'source']:
@@ -266,7 +263,18 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
if group in side_conf['group']:
group_name = side_conf['group'][group]
- fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ if family == 'ipv6' and group in ['address_group', 'network_group']:
+ fw_group = f'ipv6_{group}'
+ elif family == 'bridge':
+ if group =='ipv4_address_group':
+ fw_group = 'address_group'
+ elif group == 'ipv4_network_group':
+ fw_group = 'network_group'
+ else:
+ fw_group = group
+ else:
+ fw_group = group
+
error_group = fw_group.replace("_", "-")
if group in ['address_group', 'network_group', 'domain_group']:
@@ -302,7 +310,7 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
raise ConfigError(f'Dynamic address group must be defined.')
else:
target = rule_conf['add_address_to_group'][type]['address_group']
- fwall_group = 'ipv6_address_group' if ipv6 else 'address_group'
+ fwall_group = 'ipv6_address_group' if family == 'ipv6' else 'address_group'
group_obj = dict_search_args(firewall, 'group', 'dynamic_group', fwall_group, target)
if group_obj is None:
raise ConfigError(f'Invalid dynamic address group on firewall rule')
@@ -379,43 +387,25 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
- if 'ipv4' in firewall:
- for name in ['name','forward','input','output', 'prerouting']:
- if name in firewall['ipv4']:
- for name_id, name_conf in firewall['ipv4'][name].items():
- if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
- raise ConfigError('default-action set to jump, but no default-jump-target specified')
- if 'default_jump_target' in name_conf:
- target = name_conf['default_jump_target']
- if 'jump' not in name_conf['default_action']:
- 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.')
- 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, name, rule_conf, False)
-
- if 'ipv6' in firewall:
- for name in ['name','forward','input','output', 'prerouting']:
- if name in firewall['ipv6']:
- for name_id, name_conf in firewall['ipv6'][name].items():
- if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
- raise ConfigError('default-action set to jump, but no default-jump-target specified')
- if 'default_jump_target' in name_conf:
- target = name_conf['default_jump_target']
- if 'jump' not in name_conf['default_action']:
- 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.')
- 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, name, rule_conf, True)
-
- #### ZONESSSS
+ for family in ['ipv4', 'ipv6', 'bridge']:
+ if family in firewall:
+ for chain in ['name','forward','input','output', 'prerouting']:
+ if chain in firewall[family]:
+ for priority, priority_conf in firewall[family][chain].items():
+ if 'jump' in priority_conf['default_action'] and 'default_jump_target' not in priority_conf:
+ raise ConfigError('default-action set to jump, but no default-jump-target specified')
+ if 'default_jump_target' in priority_conf:
+ target = priority_conf['default_jump_target']
+ if 'jump' not in priority_conf['default_action']:
+ raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
+ if priority_conf['default_jump_target'] == priority:
+ raise ConfigError(f'Loop detected on default-jump-target.')
+ if target not in dict_search_args(firewall[family], 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ if 'rule' in priority_conf:
+ for rule_id, rule_conf in priority_conf['rule'].items():
+ verify_rule(firewall, family, chain, priority, rule_id, rule_conf)
+
local_zone = False
zone_interfaces = []
@@ -495,8 +485,53 @@ def generate(firewall):
render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall)
return None
+def parse_firewall_error(output):
+ # Define the regex patterns to extract the error message and the comment
+ error_pattern = re.compile(r'Error:\s*(.*?)\n')
+ comment_pattern = re.compile(r'comment\s+"([^"]+)"')
+ error_output = []
+
+ # Find all error messages in the output
+ error_matches = error_pattern.findall(output)
+ # Find all comment matches in the output
+ comment_matches = comment_pattern.findall(output)
+
+ if not error_matches or not comment_matches:
+ raise ConfigError(f'Unknown firewall error detected: {output}')
+
+ error_output.append('Fail to apply firewall')
+ # Loop over the matches and process them
+ for error_message, comment in zip(error_matches, comment_matches):
+ # Parse the comment
+ parsed_entries = comment.split('-')
+ family = 'bridge' if parsed_entries[0] == 'bri' else parsed_entries[0]
+ if parsed_entries[1] == 'NAM':
+ chain = 'name'
+ elif parsed_entries[1] == 'FWD':
+ chain = 'forward'
+ elif parsed_entries[1] == 'INP':
+ chain = 'input'
+ elif parsed_entries[1] == 'OUT':
+ chain = 'output'
+ elif parsed_entries[1] == 'PRE':
+ chain = 'prerouting'
+ error_output.append(f'Error found on: firewall {family} {chain} {parsed_entries[2]} rule {parsed_entries[3]}')
+ error_output.append(f'\tError message: {error_message.strip()}')
+
+ raise ConfigError('\n'.join(error_output))
+
def apply(firewall):
+ # Use nft -c option to check current configuration file
+ completed_process = subp_run(['nft', '-c', '--file', nftables_conf], capture_output=True)
+ install_result = completed_process.returncode
+ if install_result == 1:
+ # We need to handle firewall error
+ output = completed_process.stderr
+ parse_firewall_error(output.decode())
+
+ # No error detected during check, we can apply the new configuration
install_result, output = rc_cmd(f'nft --file {nftables_conf}')
+ # Double check just in case
if install_result == 1:
raise ConfigError(f'Failed to apply firewall: {output}')
diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py
index b9f827bee..f0a70436e 100755
--- a/src/conf_mode/interfaces_l2tpv3.py
+++ b/src/conf_mode/interfaces_l2tpv3.py
@@ -86,6 +86,8 @@ def generate(l2tpv3):
return None
def apply(l2tpv3):
+ check_kmod(k_mod)
+
# Check if L2TPv3 interface already exists
if interface_exists(l2tpv3['ifname']):
# L2TPv3 is picky when changing tunnels/sessions, thus we can simply
@@ -102,7 +104,6 @@ def apply(l2tpv3):
if __name__ == '__main__':
try:
- check_kmod(k_mod)
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index 0e0b77877..7abdfdbfa 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -103,7 +103,12 @@ def verify(wireguard):
public_keys.append(peer['public_key'])
+def generate(wireguard):
+ return None
+
def apply(wireguard):
+ check_kmod('wireguard')
+
if 'rebuild_required' in wireguard or 'deleted' in wireguard:
wg = WireGuardIf(**wireguard)
# WireGuard only supports peer removal based on the configured public-key,
@@ -123,7 +128,6 @@ def apply(wireguard):
if __name__ == '__main__':
try:
- check_kmod('wireguard')
c = get_config()
verify(c)
apply(c)
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
index f35a250cb..d24675ee6 100755
--- a/src/conf_mode/interfaces_wireless.py
+++ b/src/conf_mode/interfaces_wireless.py
@@ -239,6 +239,8 @@ def verify(wifi):
return None
def generate(wifi):
+ check_kmod('mac80211')
+
interface = wifi['ifname']
# Delete config files if interface is removed
@@ -333,7 +335,6 @@ def apply(wifi):
if __name__ == '__main__':
try:
- check_kmod('mac80211')
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index f74bb217e..39803fa02 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -240,6 +240,8 @@ def generate(nat):
return None
def apply(nat):
+ check_kmod(k_mod)
+
cmd(f'nft --file {nftables_nat_config}')
cmd(f'nft --file {nftables_static_nat_conf}')
@@ -253,7 +255,6 @@ def apply(nat):
if __name__ == '__main__':
try:
- check_kmod(k_mod)
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py
index 32a1c47d1..df501ce7f 100755
--- a/src/conf_mode/nat64.py
+++ b/src/conf_mode/nat64.py
@@ -46,7 +46,12 @@ def get_config(config: Config | None = None) -> None:
base = ["nat64"]
nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True)
- base_src = base + ["source", "rule"]
+ return nat64
+
+
+def verify(nat64) -> None:
+ check_kmod(["jool"])
+ base_src = ["nat64", "source", "rule"]
# Load in existing instances so we can destroy any unknown
lines = cmd("jool instance display --csv").splitlines()
@@ -76,12 +81,8 @@ def get_config(config: Config | None = None) -> None:
):
rules[num]["recreate"] = True
- return nat64
-
-
-def verify(nat64) -> None:
if not nat64:
- # no need to verify the CLI as nat64 is going to be deactivated
+ # nothing left to do
return
if dict_search("source.rule", nat64):
@@ -128,6 +129,9 @@ def verify(nat64) -> None:
def generate(nat64) -> None:
+ if not nat64:
+ return
+
os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
if dict_search("source.rule", nat64):
@@ -184,6 +188,7 @@ def generate(nat64) -> None:
def apply(nat64) -> None:
if not nat64:
+ unload_kmod(['jool'])
return
if dict_search("source.rule", nat64):
@@ -211,7 +216,6 @@ def apply(nat64) -> None:
if __name__ == "__main__":
try:
- check_kmod(["jool"])
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index 075738dad..c44320f36 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -112,6 +112,8 @@ def apply(nat):
if not nat:
return None
+ check_kmod(k_mod)
+
cmd(f'nft --file {nftables_nat66_config}')
call_dependents()
@@ -119,7 +121,6 @@ def apply(nat):
if __name__ == '__main__':
try:
- check_kmod(k_mod)
c = get_config()
verify(c)
generate(c)
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/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 7f6ae3680..d323ceb4f 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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
@@ -14,15 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
from ipaddress import IPv4Address
from sys import exit
from vyos import ConfigError
+from vyos import frr
from vyos.config import Config
-from vyos.utils.process import call
-from vyos.template import render
+from vyos.template import render_to_string
from vyos import airbag
airbag.enable()
@@ -92,23 +91,39 @@ def verify(mroute):
if IPv4Address(route[0]) < IPv4Address('224.0.0.0'):
raise ConfigError(route + " not a multicast network")
+
def generate(mroute):
if mroute is None:
return None
- render(config_file, 'frr/static_mcast.frr.j2', mroute)
+ mroute['new_frr_config'] = render_to_string('frr/static_mcast.frr.j2', mroute)
return None
+
def apply(mroute):
if mroute is None:
return None
+ static_daemon = 'staticd'
+
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(static_daemon)
- if os.path.exists(config_file):
- call(f'vtysh -d staticd -f {config_file}')
- os.remove(config_file)
+ if 'old_mroute' in mroute:
+ for route_gr in mroute['old_mroute']:
+ for nh in mroute['old_mroute'][route_gr]:
+ if mroute['old_mroute'][route_gr][nh]:
+ frr_cfg.modify_section(f'^ip mroute {route_gr} {nh} {mroute["old_mroute"][route_gr][nh]}')
+ else:
+ frr_cfg.modify_section(f'^ip mroute {route_gr} {nh}')
+
+ if 'new_frr_config' in mroute:
+ frr_cfg.add_before(frr.default_add_before, mroute['new_frr_config'])
+
+ frr_cfg.commit_configuration(static_daemon)
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system_acceleration.py b/src/conf_mode/system_acceleration.py
index e4b248675..d2cf44ff0 100755
--- a/src/conf_mode/system_acceleration.py
+++ b/src/conf_mode/system_acceleration.py
@@ -79,6 +79,9 @@ def verify(qat):
if not data:
raise ConfigError('No QAT acceleration device found')
+def generate(qat):
+ return
+
def apply(qat):
# Shutdown VPN service which can use QAT
if 'ipsec' in qat:
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 19bbb8875..b380e0521 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -19,6 +19,7 @@ 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 import ConfigError
@@ -74,7 +75,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 +122,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 +134,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/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index cf82b767f..b3e05a814 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -21,12 +21,15 @@ import jmespath
from sys import exit
from time import sleep
+from ipaddress import ip_address
+from netaddr import IPNetwork
+from netaddr import IPRange
-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 get_interface_dict
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
from vyos.configverify import dynamic_interface_pattern
@@ -47,6 +50,9 @@ from vyos.utils.network import interface_exists
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.process import call
+from vyos.utils.vti_updown_db import vti_updown_db_exists
+from vyos.utils.vti_updown_db import open_vti_updown_db_for_create_or_update
+from vyos.utils.vti_updown_db import remove_vti_updown_db
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -104,6 +110,8 @@ def get_config(config=None):
ipsec = config_dict_merge(default_values, ipsec)
ipsec['dhcp_interfaces'] = set()
+ ipsec['enabled_vti_interfaces'] = set()
+ ipsec['persistent_vti_interfaces'] = set()
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
@@ -121,6 +129,28 @@ def get_config(config=None):
ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
+ # Collect the interface dicts for any refernced VTI interfaces in
+ # case we need to bring the interface up
+ ipsec['vti_interface_dicts'] = {}
+
+ if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
+ for peer, peer_conf in ipsec['site_to_site']['peer'].items():
+ if 'vti' in peer_conf:
+ if 'bind' in peer_conf['vti']:
+ vti_interface = peer_conf['vti']['bind']
+ if vti_interface not in ipsec['vti_interface_dicts']:
+ _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
+ ipsec['vti_interface_dicts'][vti_interface] = vti
+
+ if 'remote_access' in ipsec:
+ if 'connection' in ipsec['remote_access']:
+ for name, ra_conf in ipsec['remote_access']['connection'].items():
+ if 'bind' in ra_conf:
+ vti_interface = ra_conf['bind']
+ if vti_interface not in ipsec['vti_interface_dicts']:
+ _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
+ ipsec['vti_interface_dicts'][vti_interface] = vti
+
return ipsec
def get_dhcp_address(iface):
@@ -249,7 +279,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on remote-access connection {name}")
- ipsec['dhcp_interfaces'].add(dhcp_interface)
+ if 'disable' not in ra_conf:
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
address = get_dhcp_address(dhcp_interface)
count = 0
@@ -304,6 +335,16 @@ def verify(ipsec):
if dict_search('remote_access.radius.server', ipsec) == None:
raise ConfigError('RADIUS authentication requires at least one server')
+ if 'bind' in ra_conf:
+ vti_interface = ra_conf['bind']
+ if not interface_exists(vti_interface):
+ raise ConfigError(f'VTI interface {vti_interface} for remote-access connection {name} does not exist!')
+
+ if 'disable' not in ra_conf:
+ ipsec['enabled_vti_interfaces'].add(vti_interface)
+ # remote access VPN interfaces are always up regardless of whether clients are connected
+ ipsec['persistent_vti_interfaces'].add(vti_interface)
+
if 'pool' in ra_conf:
if {'dhcp', 'radius'} <= set(ra_conf['pool']):
raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\
@@ -330,26 +371,73 @@ def verify(ipsec):
raise ConfigError(f'Requested pool "{pool}" does not exist!')
if 'pool' in ipsec['remote_access']:
+ pool_networks = []
for pool, pool_config in ipsec['remote_access']['pool'].items():
- if 'prefix' not in pool_config:
- raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!')
+ if 'prefix' not in pool_config and 'range' not in pool_config:
+ raise ConfigError(f'Mandatory prefix or range must be specified for pool "{pool}"!')
+
+ if 'prefix' in pool_config and 'range' in pool_config:
+ raise ConfigError(f'Only one of prefix or range can be specified for pool "{pool}"!')
+
+ if 'prefix' in pool_config:
+ range_is_ipv4 = is_ipv4(pool_config['prefix'])
+ range_is_ipv6 = is_ipv6(pool_config['prefix'])
+
+ net = IPNetwork(pool_config['prefix'])
+ start = net.first
+ stop = net.last
+ for network in pool_networks:
+ if start in network or stop in network:
+ raise ConfigError(f'Prefix for pool "{pool}" is already part of another pool\'s range!')
+
+ tmp = IPRange(start, stop)
+ pool_networks.append(tmp)
+
+ if 'range' in pool_config:
+ range_config = pool_config['range']
+ if not {'start', 'stop'} <= set(range_config.keys()):
+ raise ConfigError(f'Range start and stop address must be defined for pool "{pool}"!')
+
+ range_both_ipv4 = is_ipv4(range_config['start']) and is_ipv4(range_config['stop'])
+ range_both_ipv6 = is_ipv6(range_config['start']) and is_ipv6(range_config['stop'])
+
+ if not (range_both_ipv4 or range_both_ipv6):
+ raise ConfigError(f'Range start and stop must be of the same address family for pool "{pool}"!')
+
+ if ip_address(range_config['stop']) < ip_address(range_config['start']):
+ raise ConfigError(f'Range stop address must be greater or equal\n' \
+ 'to the range\'s start address for pool "{pool}"!')
+
+ range_is_ipv4 = is_ipv4(range_config['start'])
+ range_is_ipv6 = is_ipv6(range_config['start'])
+
+ start = range_config['start']
+ stop = range_config['stop']
+ for network in pool_networks:
+ if start in network:
+ raise ConfigError(f'Range "{range}" start address "{start}" already part of another pool\'s range!')
+ if stop in network:
+ raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another pool\'s range!')
+
+ tmp = IPRange(start, stop)
+ pool_networks.append(tmp)
if 'name_server' in pool_config:
if len(pool_config['name_server']) > 2:
raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!')
for ns in pool_config['name_server']:
- v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix'])
- v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix'])
+ v4_addr_and_ns = is_ipv4(ns) and not range_is_ipv4
+ v6_addr_and_ns = is_ipv6(ns) and not range_is_ipv6
if v4_addr_and_ns or v6_addr_and_ns:
- raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!')
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and name-server addresses!')
if 'exclude' in pool_config:
for exclude in pool_config['exclude']:
- v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix'])
- v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix'])
+ v4_addr_and_exclude = is_ipv4(exclude) and not range_is_ipv4
+ v6_addr_and_exclude = is_ipv6(exclude) and not range_is_ipv6
if v4_addr_and_exclude or v6_addr_and_exclude:
- raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!')
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and exclude prefixes!')
if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
for server, server_config in ipsec['remote_access']['radius']['server'].items():
@@ -420,7 +508,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
- ipsec['dhcp_interfaces'].add(dhcp_interface)
+ if 'disable' not in peer_conf:
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
address = get_dhcp_address(dhcp_interface)
count = 0
@@ -438,14 +527,12 @@ def verify(ipsec):
if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}")
- if dict_search('options.disable_route_autoinstall',
- ipsec) == None:
- Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]')
-
if 'bind' in peer_conf['vti']:
vti_interface = peer_conf['vti']['bind']
if not interface_exists(vti_interface):
raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')
+ if 'disable' not in peer_conf:
+ ipsec['enabled_vti_interfaces'].add(vti_interface)
if 'vti' not in peer_conf and 'tunnel' not in peer_conf:
raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}")
@@ -623,9 +710,21 @@ def apply(ipsec):
systemd_service = 'strongswan.service'
if not ipsec:
call(f'systemctl stop {systemd_service}')
+
+ if vti_updown_db_exists():
+ remove_vti_updown_db()
+
else:
call(f'systemctl reload-or-restart {systemd_service}')
+ if ipsec['enabled_vti_interfaces']:
+ with open_vti_updown_db_for_create_or_update() as db:
+ db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
+ db.setPersistentInterfaces(ipsec['persistent_vti_interfaces'])
+ db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
+ elif vti_updown_db_exists():
+ remove_vti_updown_db()
+
if ipsec.get('nhrp_exists', False):
try:
call_dependents()
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 184725573..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
@@ -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()
@@ -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}')