Skip to content

Commit

Permalink
T4916: Rewrite IPsec peer authentication and psk migration
Browse files Browse the repository at this point in the history
Rewrite strongswan IPsec authentication to reflect structure
from swanctl.conf
The most important change is that more than one local/remote ID in the
same auth entry should be allowed

replace: 'ipsec site-to-site peer <tag> authentication pre-shared-secret xxx'
      => 'ipsec authentication psk <tag> secret xxx'

set vpn ipsec authentication psk <tag> id '192.0.2.1'
set vpn ipsec authentication psk <tag> id '192.0.2.2'
set vpn ipsec authentication psk <tag> secret 'xxx'
set vpn ipsec site-to-site peer <tag> authentication local-id '192.0.2.1'
set vpn ipsec site-to-site peer <tag> authentication mode 'pre-shared-secret'
set vpn ipsec site-to-site peer <tag> authentication remote-id '192.0.2.2'
  • Loading branch information
sever-sever committed Jan 17, 2023
1 parent e59fe7c commit ca8cc37
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 35 deletions.
33 changes: 15 additions & 18 deletions data/templates/ipsec/swanctl.conf.j2
Expand Up @@ -58,23 +58,7 @@ secrets {
{% if site_to_site.peer is vyos_defined %}
{% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address and peer_conf.disable is not vyos_defined %}
{% set peer_name = peer.replace("@", "") | dot_colon_to_dash %}
{% if peer_conf.authentication.mode is vyos_defined('pre-shared-secret') %}
ike_{{ peer_name }} {
{% if peer_conf.local_address is vyos_defined %}
id-local = {{ peer_conf.local_address }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }}
{% endif %}
{% for address in peer_conf.remote_address %}
id-remote_{{ address | dot_colon_to_dash }} = {{ address }}
{% endfor %}
{% if peer_conf.authentication.local_id is vyos_defined %}
id-localid = {{ peer_conf.authentication.local_id }}
{% endif %}
{% if peer_conf.authentication.remote_id is vyos_defined %}
id-remoteid = {{ peer_conf.authentication.remote_id }}
{% endif %}
secret = "{{ peer_conf.authentication.pre_shared_secret }}"
}
{% elif peer_conf.authentication.mode is vyos_defined('x509') %}
{% if peer_conf.authentication.mode is vyos_defined('x509') %}
private_{{ peer_name }} {
file = {{ peer_conf.authentication.x509.certificate }}.pem
{% if peer_conf.authentication.x509.passphrase is vyos_defined %}
Expand All @@ -91,6 +75,20 @@ secrets {
{% endif %}
{% endfor %}
{% endif %}
{% if authentication.psk is vyos_defined %}
{% for psk, psk_config in authentication.psk.items() %}
ike-{{ psk }} {
{% if psk_config.id is vyos_defined %}
# ID's from auth psk <tag> id xxx
{% for id in psk_config.id %}
id-{{ id | dot_colon_to_dash }} = {{ id }}
{% endfor %}
{% endif %}
secret = "{{ psk_config.secret }}"
}
{% endfor %}
{% endif %}

{% if remote_access.connection is vyos_defined %}
{% for ra, ra_conf in remote_access.connection.items() if ra_conf.disable is not vyos_defined %}
{% if ra_conf.authentication.server_mode is vyos_defined('pre-shared-secret') %}
Expand Down Expand Up @@ -130,4 +128,3 @@ secrets {
{% endif %}
{% endif %}
}

2 changes: 1 addition & 1 deletion interface-definitions/include/version/ipsec-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/ipsec-version.xml.i -->
<syntaxVersion component='ipsec' version='10'></syntaxVersion>
<syntaxVersion component='ipsec' version='11'></syntaxVersion>
<!-- include end -->
34 changes: 33 additions & 1 deletion interface-definitions/vpn-ipsec.xml.in
Expand Up @@ -11,6 +11,39 @@
<priority>901</priority>
</properties>
<children>
<node name="authentication">
<properties>
<help>Authentication</help>
</properties>
<children>
<tagNode name="psk">
<properties>
<help>Pre-shared key name</help>
</properties>
<children>
<leafNode name="id">
<properties>
<help>ID for authentication</help>
<valueHelp>
<format>txt</format>
<description>ID used for authentication</description>
</valueHelp>
<multi/>
</properties>
</leafNode>
<leafNode name="secret">
<properties>
<help>IKE pre-shared secret key</help>
<valueHelp>
<format>txt</format>
<description>IKE pre-shared secret key</description>
</valueHelp>
</properties>
</leafNode>
</children>
</tagNode>
</children>
</node>
<leafNode name="disable-uniqreqids">
<properties>
<help>Disable requirement for unique IDs in the Security Database</help>
Expand Down Expand Up @@ -948,7 +981,6 @@
</constraint>
</properties>
</leafNode>
#include <include/ipsec/authentication-pre-shared-secret.xml.i>
<leafNode name="remote-id">
<properties>
<help>ID for remote authentication</help>
Expand Down
55 changes: 41 additions & 14 deletions smoketest/scripts/cli/test_vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021-2022 VyOS maintainers and contributors
# Copyright (C) 2021-2023 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
Expand Down Expand Up @@ -34,6 +34,8 @@

peer_ip = '203.0.113.45'
connection_name = 'main-branch'
local_id = 'left'
remote_id = 'right'
interface = 'eth1'
vif = '100'
esp_group = 'MyESPGroup'
Expand Down Expand Up @@ -151,10 +153,15 @@ def test_01_dhcp_fail_handling(self):
# Interface for dhcp-interface
self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server

# vpn ipsec auth psk <tag> id <x.x.x.x>
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])

# Site to site
peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
self.cli_set(peer_base_path + ['ike-group', ike_group])
self.cli_set(peer_base_path + ['default-esp-group', esp_group])
self.cli_set(peer_base_path + ['dhcp-interface', f'{interface}.{vif}'])
Expand All @@ -172,18 +179,25 @@ def test_01_dhcp_fail_handling(self):
def test_02_site_to_site(self):
self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])

# Site to site
local_address = '192.0.2.10'
priority = '20'
life_bytes = '100000'
life_packets = '2000000'

# vpn ipsec auth psk <tag> id <x.x.x.x>
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])

# Site to site
peer_base_path = base_path + ['site-to-site', 'peer', connection_name]

self.cli_set(base_path + ['esp-group', esp_group, 'life-bytes', life_bytes])
self.cli_set(base_path + ['esp-group', esp_group, 'life-packets', life_packets])

self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
self.cli_set(peer_base_path + ['ike-group', ike_group])
self.cli_set(peer_base_path + ['default-esp-group', esp_group])
self.cli_set(peer_base_path + ['local-address', local_address])
Expand Down Expand Up @@ -230,8 +244,10 @@ def test_02_site_to_site(self):
self.assertIn(line, swanctl_conf)

swanctl_secrets_lines = [
f'id-local = {local_address} # dhcp:no',
f'id-remote_{peer_ip.replace(".","-")} = {peer_ip}',
f'id-{local_id} = {local_id}',
f'id-{remote_id} = {remote_id}',
f'id-{local_address.replace(".","-")} = {local_address}',
f'id-{peer_ip.replace(".","-")} = {peer_ip}',
f'secret = "{secret}"'
]
for line in swanctl_secrets_lines:
Expand All @@ -249,10 +265,15 @@ def test_03_site_to_site_vti(self):
# VTI interface
self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24'])

# vpn ipsec auth psk <tag> id <x.x.x.x>
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])

# Site to site
peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
self.cli_set(peer_base_path + ['connection-type', 'none'])
self.cli_set(peer_base_path + ['force-udp-encapsulation'])
self.cli_set(peer_base_path + ['ike-group', ike_group])
Expand Down Expand Up @@ -295,8 +316,8 @@ def test_03_site_to_site_vti(self):
self.assertIn(line, swanctl_conf)

swanctl_secrets_lines = [
f'id-local = {local_address} # dhcp:no',
f'id-remote_{peer_ip.replace(".","-")} = {peer_ip}',
f'id-{local_id} = {local_id}',
f'id-{remote_id} = {remote_id}',
f'secret = "{secret}"'
]
for line in swanctl_secrets_lines:
Expand Down Expand Up @@ -453,9 +474,15 @@ def test_06_flex_vpn_vips(self):
self.cli_set(base_path + ['options', 'interface', 'tun1'])
self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])

# vpn ipsec auth psk <tag> id <x.x.x.x>
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])

self.cli_set(peer_base_path + ['authentication', 'local-id', local_id])
self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
self.cli_set(peer_base_path + ['authentication', 'remote-id', remote_id])
self.cli_set(peer_base_path + ['connection-type', 'initiate'])
self.cli_set(peer_base_path + ['ike-group', ike_group])
Expand Down Expand Up @@ -485,10 +512,10 @@ def test_06_flex_vpn_vips(self):
self.assertIn(line, swanctl_conf)

swanctl_secrets_lines = [
f'id-local = {local_address} # dhcp:no',
f'id-remote_{peer_ip.replace(".","-")} = {peer_ip}',
f'id-localid = {local_id}',
f'id-remoteid = {remote_id}',
f'id-{local_id} = {local_id}',
f'id-{remote_id} = {remote_id}',
f'id-{peer_ip.replace(".","-")} = {peer_ip}',
f'id-{local_address.replace(".","-")} = {local_address}',
f'secret = "{secret}"',
]

Expand Down
8 changes: 7 additions & 1 deletion src/conf_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021-2022 VyOS maintainers and contributors
# Copyright (C) 2021-2023 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
Expand Down Expand Up @@ -219,6 +219,12 @@ def verify(ipsec):
if not ipsec:
return None

if 'authentication' in ipsec:
if 'psk' in ipsec['authentication']:
for psk, psk_config in ipsec['authentication']['psk'].items():
if 'id' not in psk_config or 'secret' not in psk_config:
raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"')

if 'interfaces' in ipsec :
for ifname in ipsec['interface']:
verify_interface_exists(ifname)
Expand Down
74 changes: 74 additions & 0 deletions src/migration-scripts/ipsec/10-to-11
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
#
# Copyright (C) 2023 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import re

from sys import argv
from sys import exit

from vyos.configtree import ConfigTree


if (len(argv) < 1):
print("Must specify file name!")
exit(1)

file_name = argv[1]

with open(file_name, 'r') as f:
config_file = f.read()

base = ['vpn', 'ipsec']
config = ConfigTree(config_file)

if not config.exists(base):
# Nothing to do
exit(0)

# PEER changes
if config.exists(base + ['site-to-site', 'peer']):
for peer in config.list_nodes(base + ['site-to-site', 'peer']):
peer_base = base + ['site-to-site', 'peer', peer]

# replace: 'ipsec site-to-site peer <tag> authentication pre-shared-secret xxx'
# => 'ipsec authentication psk <tag> secret xxx'
if config.exists(peer_base + ['authentication', 'pre-shared-secret']):
tmp = config.return_value(peer_base + ['authentication', 'pre-shared-secret'])
config.delete(peer_base + ['authentication', 'pre-shared-secret'])
config.set(base + ['authentication', 'psk', peer, 'secret'], value=tmp)

# Get id's from peers for "ipsec auth psk <tag> id xxx"
if config.exists(peer_base + ['authentication', 'local-id']):
local_id = config.return_value(peer_base + ['authentication', 'local-id'])
config.set(base + ['authentication', 'psk', peer, 'id'], value=local_id, replace=False)
if config.exists(peer_base + ['authentication', 'remote-id']):
remote_id = config.return_value(peer_base + ['authentication', 'remote-id'])
config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_id, replace=False)

if config.exists(peer_base + ['local-address']):
tmp = config.return_value(peer_base + ['local-address'])
config.set(base + ['authentication', 'psk', peer, 'id'], value=tmp, replace=False)
if config.exists(peer_base + ['remote-address']):
tmp = config.return_value(peer_base + ['remote-address'])
config.set(base + ['authentication', 'psk', peer, 'id'], value=tmp, replace=False)


try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print(f'Failed to save the modified config: {e}')
exit(1)

0 comments on commit ca8cc37

Please sign in to comment.