summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@vyos.io>2026-01-08 15:29:09 +0000
committerGitHub <noreply@github.com>2026-01-08 15:29:09 +0000
commitf80aae18439fe93a32101dfe45ef65812cf91e45 (patch)
treee6cee6d8619875506e0a3b1888e31d831a76a18c
parentfc945ea7ba90d86142984b992bc3e79ddf6b0ece (diff)
parent875cdbe042aa3c178f033b5699763ebcaf5261cd (diff)
downloadvyos-1x-f80aae18439fe93a32101dfe45ef65812cf91e45.tar.gz
vyos-1x-f80aae18439fe93a32101dfe45ef65812cf91e45.zip
Merge pull request #4887 from alexandr-san4ez/T8022-current
ipsec: T8022: Fix invalid automatic `dynamic` prefix assignment for transport mode tunnels
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py85
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py54
2 files changed, 139 insertions, 0 deletions
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 637e854a2..9461cd183 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -419,6 +419,91 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
for line in swanctl_conf_lines:
self.assertIn(line, swanctl_conf)
+ def test_site_to_site_gre_over_ipsec(self):
+ """Test GRE over IPsec site‑to‑site configuration with transport mode ESP"""
+
+ tunnel_id = '100'
+ local_address = '172.168.99.2'
+
+ # Interfaces
+ base_tun_path = tunnel_path + [f'tun{tunnel_id}']
+ self.cli_set(base_tun_path + ['address', '10.12.0.1/30'])
+ self.cli_set(base_tun_path + ['encapsulation', 'gre'])
+ self.cli_set(base_tun_path + ['remote', peer_ip])
+ self.cli_set(base_tun_path + ['source-address', local_address])
+ self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp'])
+
+ # Authentication (PSK)
+ base_psk_path = base_path + ['authentication', 'psk']
+ self.cli_set(base_psk_path + [peer_name, 'id', local_address])
+ self.cli_set(base_psk_path + [peer_name, 'id', peer_ip])
+ self.cli_set(base_psk_path + [peer_name, 'secret', secret])
+
+ # ESP group
+ base_esp_path = base_path + ['esp-group', esp_group]
+ self.cli_set(base_esp_path + ['lifetime', '3600'])
+ self.cli_set(base_esp_path + ['mode', 'transport'])
+ self.cli_set(base_esp_path + ['pfs', 'dh-group14'])
+ self.cli_set(base_esp_path + ['proposal', '10', 'encryption', 'aes256'])
+ self.cli_set(base_esp_path + ['proposal', '10', 'hash', 'sha1'])
+
+ # IKE group
+ base_ike_path = base_path + ['ike-group', ike_group]
+ self.cli_set(base_ike_path + ['close-action', 'none'])
+ self.cli_set(base_ike_path + ['dead-peer-detection', 'action', 'restart'])
+ self.cli_set(base_ike_path + ['dead-peer-detection', 'interval', '10'])
+ self.cli_set(base_ike_path + ['key-exchange', 'ikev2'])
+ self.cli_set(base_ike_path + ['lifetime', '28800'])
+ self.cli_set(base_ike_path + ['proposal', '10', 'dh-group', '5'])
+ self.cli_set(base_ike_path + ['proposal', '10', 'encryption', 'aes256'])
+ self.cli_set(base_ike_path + ['proposal', '10', 'hash', 'sha1'])
+
+ # IPsec interface binding
+ self.cli_set(base_path + ['interface', interface])
+
+ # Site‑to‑site peer
+ peer_path = base_path + ['site-to-site', 'peer', peer_name]
+ self.cli_set(peer_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_path + ['authentication', 'local-id', local_address])
+ self.cli_set(peer_path + ['authentication', 'remote-id', peer_ip])
+ self.cli_set(peer_path + ['connection-type', 'initiate'])
+ self.cli_set(peer_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_path + ['ike-group', ike_group])
+ self.cli_set(peer_path + ['local-address', local_address])
+ self.cli_set(peer_path + ['remote-address', peer_ip])
+ self.cli_set(peer_path + ['tunnel', tunnel_id, 'protocol', 'gre'])
+
+ # Commit and verify
+ self.cli_commit()
+
+ # Verify strongSwan configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_conf_lines = [
+ 'version = 2',
+ 'auth = psk',
+ 'proposals = aes128-sha1-modp1024,aes256-sha1-modp1536',
+ 'esp_proposals = aes128-sha1-modp2048,aes256-sha1-modp2048',
+ 'life_time = 3600s',
+ 'mode = transport', # ensure transport mode is used
+ f'{peer_name}-tunnel-{tunnel_id}',
+ f'local_ts = {local_address}[gre/]', # GRE tunnel source/target
+ f'remote_ts = {peer_ip}[gre/]',
+ f'local_addrs = {local_address} # dhcp:no',
+ f'remote_addrs = {peer_ip}',
+ ]
+ for line in swanctl_conf_lines:
+ with self.subTest(line=line):
+ self.assertIn(line, swanctl_conf)
+
+ # Verify validation of local/remote prefix
+ base_tun_path = peer_path + ['tunnel', tunnel_id]
+ self.cli_set(base_tun_path + ['local', 'prefix', '10.1.2.0/24'])
+ self.cli_set(base_tun_path + ['remote', 'prefix', '10.4.5.0/24'])
+
+ err_msg = 'Local/remote prefix cannot be used with ESP transport mode on tunnel'
+ with self.assertRaisesRegex(ConfigSessionError, err_msg):
+ self.cli_commit()
+
def test_site_to_site_vti(self):
local_address = '192.0.2.10'
vti = 'vti10'
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index dcb9f9c69..c6492f513 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -80,6 +80,57 @@ CRL_PATH = f'{swanctl_dir}/x509crl/'
DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_interfaces'
+
+def _cleanup_default_prefixes(ipsec: dict, default_values: dict):
+ """
+ Remove default local/remote prefixes from tunnels
+ that use 'transport' mode ESP and do not have explicit prefix definitions
+ """
+ site_to_site = dict_search_args(ipsec, 'site_to_site', 'peer') or {}
+
+ for peer, peer_conf in site_to_site.items():
+ tunnels = peer_conf.get('tunnel') or {}
+ default_esp_group = peer_conf.get('default_esp_group')
+
+ for tunnel, tunnel_conf in tunnels.items():
+ # Determine ESP group name - prefer specific over default
+ tunnel_esp_group = tunnel_conf.get('esp_group')
+ esp_group_name = tunnel_esp_group or default_esp_group
+
+ # Get default values for the tunnel
+ tunnel_defaults = dict_search_args(
+ default_values, 'site_to_site', 'peer', peer, 'tunnel', tunnel
+ )
+
+ # Skip if no defaults found or ESP group defined
+ # Yes, this can happen because of user misconfiguration
+ if tunnel_defaults is None or esp_group_name is None:
+ continue
+
+ # Fetch ESP group details
+ esp_group_mode = dict_search_args(
+ ipsec, 'esp_group', esp_group_name, 'mode'
+ )
+
+ # Only act if ESP group is in transport mode
+ if esp_group_mode == 'transport':
+
+ # Look for local and remote prefixes
+ local_prefixes = dict_search_args(tunnel_conf, 'local', 'prefix')
+ remote_prefixes = dict_search_args(tunnel_conf, 'remote', 'prefix')
+
+ # Safely remove missing prefixes from defaults
+ # if user has not defined them but they are in defaults
+ if not local_prefixes:
+ prefix = dict_search_args(tunnel_defaults, 'local', 'prefix')
+ if prefix is not None:
+ del tunnel_defaults['local']['prefix']
+
+ if not remote_prefixes:
+ prefix = dict_search_args(tunnel_defaults, 'remote', 'prefix')
+ if prefix is not None:
+ del tunnel_defaults['remote']['prefix']
+
def get_config(config=None):
if config:
conf = config
@@ -114,6 +165,9 @@ def get_config(config=None):
if 'dead_peer_detection' not in ike:
del default_values['ike_group'][name]['dead_peer_detection']
+ # Clean up default prefixes for ESP transport-mode tunnels
+ _cleanup_default_prefixes(ipsec, default_values)
+
ipsec = config_dict_merge(default_values, ipsec)
ipsec['dhcp_interfaces'] = set()