summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/add-rebase-label.yml14
-rw-r--r--data/templates/ipsec/swanctl/peer.j210
-rw-r--r--data/templates/ipsec/swanctl/remote_access.j218
-rw-r--r--interface-definitions/include/version/openvpn-version.xml.i2
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in12
-rw-r--r--interface-definitions/vpn_ipsec.xml.in6
-rw-r--r--python/vyos/ifconfig/interface.py28
-rw-r--r--python/vyos/opmode.py10
-rw-r--r--python/vyos/utils/network.py23
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py46
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py14
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static.py5
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py130
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py7
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py7
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py16
-rwxr-xr-xsrc/conf_mode/vrf.py5
-rw-r--r--src/migration-scripts/openvpn/2-to-343
-rwxr-xr-xsrc/op_mode/bridge.py2
-rwxr-xr-xsrc/op_mode/dhcp.py2
-rwxr-xr-xsrc/op_mode/openconnect.py6
-rwxr-xr-xsrc/op_mode/ssh.py2
-rw-r--r--src/op_mode/zone.py4
-rw-r--r--src/services/api/graphql/session/errors/op_mode_errors.py2
24 files changed, 360 insertions, 54 deletions
diff --git a/.github/workflows/add-rebase-label.yml b/.github/workflows/add-rebase-label.yml
new file mode 100644
index 000000000..9041303af
--- /dev/null
+++ b/.github/workflows/add-rebase-label.yml
@@ -0,0 +1,14 @@
+name: Add rebase label
+
+on:
+ pull_request_target:
+ types: [synchronize, opened, reopened, labeled, unlabeled]
+
+permissions:
+ pull-requests: write
+ contents: read
+
+jobs:
+ add-rebase-label:
+ uses: vyos/.github/.github/workflows/add-rebase-label.yml@current
+ secrets: inherit
diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2
index 58f0199fa..3a9af2c94 100644
--- a/data/templates/ipsec/swanctl/peer.j2
+++ b/data/templates/ipsec/swanctl/peer.j2
@@ -63,6 +63,11 @@
life_packets = {{ vti_esp.life_packets }}
{% endif %}
life_time = {{ vti_esp.lifetime }}s
+{% if vti_esp.disable_rekey is vyos_defined %}
+ rekey_bytes = 0
+ rekey_packets = 0
+ rekey_time = 0s
+{% endif %}
local_ts = 0.0.0.0/0,::/0
remote_ts = 0.0.0.0/0,::/0
updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}"
@@ -108,6 +113,11 @@
life_packets = {{ tunnel_esp.life_packets }}
{% endif %}
life_time = {{ tunnel_esp.lifetime }}s
+{% if tunnel_esp.disable_rekey is vyos_defined %}
+ rekey_bytes = 0
+ rekey_packets = 0
+ rekey_time = 0s
+{% endif %}
{% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %}
{% if tunnel_conf.local.prefix is vyos_defined %}
{% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %}
diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2
index 6bced88c7..e384ae972 100644
--- a/data/templates/ipsec/swanctl/remote_access.j2
+++ b/data/templates/ipsec/swanctl/remote_access.j2
@@ -8,6 +8,10 @@
proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }}
version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }}
send_certreq = no
+{% if ike.dead_peer_detection is vyos_defined %}
+ dpd_timeout = {{ ike.dead_peer_detection.timeout }}
+ dpd_delay = {{ ike.dead_peer_detection.interval }}
+{% endif %}
rekey_time = {{ ike.lifetime }}s
keyingtries = 0
{% if rw_conf.unique is vyos_defined %}
@@ -44,8 +48,18 @@
children {
ikev2-vpn {
esp_proposals = {{ esp | get_esp_ike_cipher(ike) | join(',') }}
- rekey_time = {{ esp.lifetime }}s
- rand_time = 540s
+{% if esp.life_bytes is vyos_defined %}
+ life_bytes = {{ esp.life_bytes }}
+{% endif %}
+{% if esp.life_packets is vyos_defined %}
+ life_packets = {{ esp.life_packets }}
+{% endif %}
+ life_time = {{ esp.lifetime }}s
+{% if esp.disable_rekey is vyos_defined %}
+ rekey_bytes = 0
+ rekey_packets = 0
+ rekey_time = 0s
+{% endif %}
dpd_action = clear
inactivity = {{ rw_conf.timeout }}
{% if rw_conf.replay_window is vyos_defined %}
diff --git a/interface-definitions/include/version/openvpn-version.xml.i b/interface-definitions/include/version/openvpn-version.xml.i
index e4eb13b7c..e03ad55c0 100644
--- a/interface-definitions/include/version/openvpn-version.xml.i
+++ b/interface-definitions/include/version/openvpn-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/openvpn-version.xml.i -->
-<syntaxVersion component='openvpn' version='2'></syntaxVersion>
+<syntaxVersion component='openvpn' version='3'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in
index 23cc83e9a..1860523c2 100644
--- a/interface-definitions/interfaces_openvpn.xml.in
+++ b/interface-definitions/interfaces_openvpn.xml.in
@@ -589,25 +589,25 @@
<properties>
<help>Topology for clients</help>
<completionHelp>
- <list>net30 point-to-point subnet</list>
+ <list>subnet point-to-point net30</list>
</completionHelp>
<valueHelp>
- <format>net30</format>
- <description>net30 topology</description>
+ <format>subnet</format>
+ <description>Subnet topology (recommended)</description>
</valueHelp>
<valueHelp>
<format>point-to-point</format>
<description>Point-to-point topology</description>
</valueHelp>
<valueHelp>
- <format>subnet</format>
- <description>Subnet topology</description>
+ <format>net30</format>
+ <description>net30 topology (deprecated)</description>
</valueHelp>
<constraint>
<regex>(subnet|point-to-point|net30)</regex>
</constraint>
</properties>
- <defaultValue>net30</defaultValue>
+ <defaultValue>subnet</defaultValue>
</leafNode>
<node name="mfa">
<properties>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 7f425d982..4a7fde75b 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -99,6 +99,12 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="disable-rekey">
+ <properties>
+ <help>Do not locally initiate a re-key of the SA, remote peer must re-key before expiration</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="mode">
<properties>
<help>ESP mode</help>
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 117479ade..748830004 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -37,6 +37,7 @@ from vyos.utils.network import mac2eui64
from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_interface_namespace
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import is_netns_interface
from vyos.utils.process import is_systemd_service_active
from vyos.utils.process import run
@@ -402,7 +403,7 @@ class Interface(Control):
if netns: cmd = f'ip netns exec {netns} {cmd}'
return self._cmd(cmd)
- def _set_vrf_ct_zone(self, vrf):
+ def _set_vrf_ct_zone(self, vrf, old_vrf_tableid=None):
"""
Add/Remove rules in nftables to associate traffic in VRF to an
individual conntack zone
@@ -411,20 +412,27 @@ class Interface(Control):
if 'netns' in self.config:
return None
+ def nft_check_and_run(nft_command):
+ # Check if deleting is possible first to avoid raising errors
+ _, err = self._popen(f'nft --check {nft_command}')
+ if not err:
+ # Remove map element
+ self._cmd(f'nft {nft_command}')
+
if vrf:
# Get routing table ID for VRF
- vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get(
- 'info_data', {}).get('table')
+ vrf_table_id = get_vrf_tableid(vrf)
# Add map element with interface and zone ID
if vrf_table_id:
+ # delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF
+ if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id):
+ nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
+ nft_check_and_run(nft_del_element)
+
self._cmd(f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}')
else:
nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
- # Check if deleting is possible first to avoid raising errors
- _, err = self._popen(f'nft --check {nft_del_element}')
- if not err:
- # Remove map element
- self._cmd(f'nft {nft_del_element}')
+ nft_check_and_run(nft_del_element)
def get_min_mtu(self):
"""
@@ -601,8 +609,10 @@ class Interface(Control):
if tmp == vrf:
return False
+ # Get current VRF table ID
+ old_vrf_tableid = get_vrf_tableid(self.ifname)
self.set_interface('vrf', vrf)
- self._set_vrf_ct_zone(vrf)
+ self._set_vrf_ct_zone(vrf, old_vrf_tableid)
return True
def set_arp_cache_tmo(self, tmo):
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 8dab9a4ca..a9819dc4b 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -31,7 +31,15 @@ class Error(Exception):
class UnconfiguredSubsystem(Error):
""" Requested operation is valid, but cannot be completed
- because corresponding subsystem is not configured and running.
+ because corresponding subsystem is not configured
+ and thus is not running.
+ """
+ pass
+
+class UnconfiguredObject(UnconfiguredSubsystem):
+ """ Requested operation is valid but cannot be completed
+ because its parameter refers to an object that does not exist
+ in the system configuration.
"""
pass
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 829124b57..8406a5638 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -83,6 +83,19 @@ def get_interface_vrf(interface):
return tmp['master']
return 'default'
+def get_vrf_tableid(interface: str):
+ """ Return VRF table ID for given interface name or None """
+ from vyos.utils.dict import dict_search
+ table = None
+ tmp = get_interface_config(interface)
+ # Check if we are "the" VRF interface
+ if dict_search('linkinfo.info_kind', tmp) == 'vrf':
+ table = tmp['linkinfo']['info_data']['table']
+ # or an interface bound to a VRF
+ elif dict_search('linkinfo.info_slave_kind', tmp) == 'vrf':
+ table = tmp['linkinfo']['info_slave_data']['table']
+ return table
+
def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
@@ -537,21 +550,21 @@ def ipv6_prefix_length(low, high):
return None
xor = bytearray(a ^ b for a, b in zip(lo, hi))
-
+
plen = 0
while plen < 128 and xor[plen // 8] == 0:
plen += 8
-
+
if plen == 128:
return plen
-
+
for i in range((plen // 8) + 1, 16):
if xor[i] != 0:
return None
-
+
for i in range(8):
msk = ~xor[plen // 8] & 0xff
-
+
if msk == bytemasks[i]:
return plen + i + 1
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 9be2c2f1a..4072fd5c2 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -28,6 +28,7 @@ from vyos.utils.dict import dict_search
from vyos.utils.process import process_named_running
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_interface_vrf
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.process import cmd
from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import is_ipv6_link_local
@@ -257,6 +258,51 @@ class BasicInterfaceTest:
self.cli_delete(['vrf', 'name', vrf_name])
+ def test_move_interface_between_vrf_instances(self):
+ if not self._test_vrf:
+ self.skipTest('not supported')
+
+ vrf1_name = 'smoketest_mgmt1'
+ vrf1_table = '5424'
+ vrf2_name = 'smoketest_mgmt2'
+ vrf2_table = '7412'
+
+ self.cli_set(['vrf', 'name', vrf1_name, 'table', vrf1_table])
+ self.cli_set(['vrf', 'name', vrf2_name, 'table', vrf2_table])
+
+ # move interface into first VRF
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+ self.cli_set(self._base_path + [interface, 'vrf', vrf1_name])
+
+ self.cli_commit()
+
+ # check that interface belongs to proper VRF
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf1_name)
+
+ tmp = get_interface_config(vrf1_name)
+ self.assertEqual(int(vrf1_table), get_vrf_tableid(interface))
+
+ # move interface into second VRF
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'vrf', vrf2_name])
+
+ self.cli_commit()
+
+ # check that interface belongs to proper VRF
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf2_name)
+
+ tmp = get_interface_config(vrf2_name)
+ self.assertEqual(int(vrf2_table), get_vrf_tableid(interface))
+
+ self.cli_delete(['vrf', 'name', vrf1_name])
+ self.cli_delete(['vrf', 'name', vrf2_name])
+
def test_span_mirror(self):
if not self._mirror_interfaces:
self.skipTest('not supported')
diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
index 585c1dc89..905eaf2e9 100755
--- a/smoketest/scripts/cli/test_protocols_ospf.py
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -16,7 +16,6 @@
import unittest
-from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
@@ -27,6 +26,7 @@ PROCESS_NAME = 'ospfd'
base_path = ['protocols', 'ospf']
route_map = 'foo-bar-baz10'
+dummy_if = 'dum3562'
class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -38,6 +38,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit'])
+ cls.cli_set(cls, ['interfaces', 'dummy', dummy_if])
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
@@ -46,6 +47,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
@classmethod
def tearDownClass(cls):
cls.cli_delete(cls, ['policy', 'route-map', route_map])
+ cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if])
super(TestProtocolsOSPF, cls).tearDownClass()
def tearDown(self):
@@ -441,14 +443,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
global_block_high = "399"
local_block_low = "400"
local_block_high = "499"
- interface = 'lo'
maximum_stack_size = '5'
prefix_one = '192.168.0.1/32'
prefix_two = '192.168.0.2/32'
prefix_one_value = '1'
prefix_two_value = '2'
- self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', dummy_if])
self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size])
self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low])
self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high])
@@ -472,17 +473,14 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
def test_ospf_15_ldp_sync(self):
holddown = "500"
- interface = 'lo'
interfaces = Section.interfaces('ethernet')
- self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', dummy_if])
self.cli_set(base_path + ['ldp-sync', 'holddown', holddown])
# Commit main OSPF changes
self.cli_commit()
- sleep(10)
-
# Verify main OSPF changes
frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
self.assertIn(f'router ospf', frrconfig)
@@ -514,7 +512,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
self.assertIn(f'interface {interface}', config)
self.assertIn(f' ip ospf dead-interval 40', config)
- self.assertIn(f' no ip ospf mpls ldp-sync', config)
+ self.assertNotIn(f' ip ospf mpls ldp-sync', config)
def test_ospf_16_graceful_restart(self):
period = '300'
diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py
index c5cf2aab6..f676e2a52 100755
--- a/smoketest/scripts/cli/test_protocols_static.py
+++ b/smoketest/scripts/cli/test_protocols_static.py
@@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv6
from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
base_path = ['protocols', 'static']
vrf_path = ['protocols', 'vrf']
@@ -421,7 +422,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
tmp = get_interface_config(vrf)
# Compare VRF table ID
- self.assertEqual(tmp['linkinfo']['info_data']['table'], int(vrf_config['table']))
+ self.assertEqual(get_vrf_tableid(vrf), int(vrf_config['table']))
self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf')
# Verify FRR bgpd configuration
@@ -478,4 +479,4 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
self.assertIn(tmp, frrconfig)
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ unittest.main(verbosity=2, failfast=True)
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 27356d70e..2dc66485b 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -252,6 +252,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
for line in swanctl_conf_lines:
self.assertIn(line, swanctl_conf)
+ # if dpd is not specified it should not be enabled (see T6599)
+ swanctl_unexpected_lines = [
+ f'dpd_timeout'
+ f'dpd_delay'
+ ]
+
+ for unexpected_line in swanctl_unexpected_lines:
+ self.assertNotIn(unexpected_line, swanctl_conf)
+
swanctl_secrets_lines = [
f'id-{regex_uuid4} = "{local_id}"',
f'id-{regex_uuid4} = "{remote_id}"',
@@ -639,8 +648,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'auth = eap-mschapv2',
f'eap_id = %any',
f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
- f'rekey_time = {eap_lifetime}s',
- f'rand_time = 540s',
+ f'life_time = {eap_lifetime}s',
f'dpd_action = clear',
f'replay_window = 32',
f'inactivity = 28800',
@@ -761,8 +769,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'auth = eap-tls',
f'eap_id = %any',
f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
- f'rekey_time = {eap_lifetime}s',
- f'rand_time = 540s',
+ f'life_time = {eap_lifetime}s',
f'dpd_action = clear',
f'inactivity = 28800',
f'local_ts = 0.0.0.0/0,::/0',
@@ -876,8 +883,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'certs = peer1.pem',
f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem',
f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
- f'rekey_time = {eap_lifetime}s',
- f'rand_time = 540s',
+ f'life_time = {eap_lifetime}s',
f'dpd_action = clear',
f'inactivity = 28800',
f'local_ts = 0.0.0.0/0,::/0',
@@ -968,5 +974,117 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.tearDownPKI()
+ def test_remote_access_no_rekey(self):
+ # In some RA secnarios, disabling server-initiated rekey of IKE and CHILD SA is desired
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', '0'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'disable-rekey'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ # Use client-mode x509 instead of default EAP-MSCHAPv2
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', int_ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = 0s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'rekey_time = 0s',
+ f'dpd_action = clear',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {prefix}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index 243397dc2..176882ca5 100755
--- a/smoketest/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -25,6 +25,7 @@ from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.utils.file import read_file
from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import interface_exists
from vyos.utils.system import sysctl_read
@@ -111,8 +112,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
frrconfig = self.getFRRconfig(f'vrf {vrf}')
self.assertIn(f' vni {table}', frrconfig)
- tmp = get_interface_config(vrf)
- self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+ self.assertEqual(int(table), get_vrf_tableid(vrf))
# Increment table ID for the next run
table = str(int(table) + 1)
@@ -266,8 +266,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for address in addresses:
self.assertTrue(is_intf_addr_assigned(interface, address))
# Verify VRF table ID
- tmp = get_interface_config(vrf)
- self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+ self.assertEqual(int(table), get_vrf_tableid(vrf))
# Verify interface is assigned to VRF
tmp = get_interface_config(interface)
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 017010a61..0dc76b39a 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -432,6 +432,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 = ''
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..12edb7d02 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -27,6 +27,7 @@ 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
@@ -160,8 +161,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
diff --git a/src/migration-scripts/openvpn/2-to-3 b/src/migration-scripts/openvpn/2-to-3
new file mode 100644
index 000000000..0b9073ae6
--- /dev/null
+++ b/src/migration-scripts/openvpn/2-to-3
@@ -0,0 +1,43 @@
+#!/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:
+ if not config.exists(['interfaces', 'openvpn']):
+ # Nothing to do
+ return
+
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
+ 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/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/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/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/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/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/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,