diff options
| -rw-r--r-- | data/templates/ipsec/swanctl.conf.j2 | 34 | ||||
| -rw-r--r-- | interface-definitions/include/dhcp-interface-multi.xml.i | 18 | ||||
| -rw-r--r-- | interface-definitions/include/version/ipsec-version.xml.i | 2 | ||||
| -rw-r--r-- | interface-definitions/vpn-ipsec.xml.in | 35 | ||||
| -rw-r--r-- | python/vyos/template.py | 10 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_vpn_ipsec.py | 62 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 17 | ||||
| -rwxr-xr-x | src/migration-scripts/ipsec/10-to-11 | 85 | 
8 files changed, 225 insertions, 38 deletions
| diff --git a/data/templates/ipsec/swanctl.conf.j2 b/data/templates/ipsec/swanctl.conf.j2 index 38d7981c6..d44d0f5e4 100644 --- a/data/templates/ipsec/swanctl.conf.j2 +++ b/data/templates/ipsec/swanctl.conf.j2 @@ -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 %} @@ -91,6 +75,21 @@ 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 %} +{%                 set gen_uuid = '' | generate_uuid4 %} +        id-{{ gen_uuid }} = "{{ 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') %} @@ -130,4 +129,3 @@ secrets {  {%     endif %}  {% endif %}  } - diff --git a/interface-definitions/include/dhcp-interface-multi.xml.i b/interface-definitions/include/dhcp-interface-multi.xml.i new file mode 100644 index 000000000..c74751a19 --- /dev/null +++ b/interface-definitions/include/dhcp-interface-multi.xml.i @@ -0,0 +1,18 @@ +<!-- include start from dhcp-interface-multi.xml.i --> +<leafNode name="dhcp-interface"> +  <properties> +    <help>DHCP interface supplying next-hop IP address</help> +    <completionHelp> +      <script>${vyos_completion_dir}/list_interfaces.py</script> +    </completionHelp> +    <valueHelp> +      <format>txt</format> +      <description>DHCP interface name</description> +    </valueHelp> +    <constraint> +      #include <include/constraint/interface-name.xml.in> +    </constraint> +    <multi/> +  </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/version/ipsec-version.xml.i b/interface-definitions/include/version/ipsec-version.xml.i index 1c978e8e6..8d019b466 100644 --- a/interface-definitions/include/version/ipsec-version.xml.i +++ b/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 --> diff --git a/interface-definitions/vpn-ipsec.xml.in b/interface-definitions/vpn-ipsec.xml.in index 4bb9ad145..9d20926ec 100644 --- a/interface-definitions/vpn-ipsec.xml.in +++ b/interface-definitions/vpn-ipsec.xml.in @@ -11,6 +11,40 @@            <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> +                  #include <include/dhcp-interface-multi.xml.i> +                  <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> @@ -987,7 +1021,6 @@                            </constraint>                          </properties>                        </leafNode> -                      #include <include/ipsec/authentication-pre-shared-secret.xml.i>                        <leafNode name="remote-id">                          <properties>                            <help>ID for remote authentication</help> diff --git a/python/vyos/template.py b/python/vyos/template.py index ce9983958..15240f815 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -193,6 +193,16 @@ def dot_colon_to_dash(text):      text = text.replace(".", "-")      return text +@register_filter('generate_uuid4') +def generate_uuid4(text): +    """ Generate random unique ID +    Example: +      % uuid4() +      UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5') +    """ +    from uuid import uuid4 +    return uuid4() +  @register_filter('netmask_from_cidr')  def netmask_from_cidr(prefix):      """ Take CIDR prefix and convert the prefix length to a "subnet mask". diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 03780c465..c8634dd57 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/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 @@ -34,12 +34,15 @@ swanctl_file = '/etc/swanctl/swanctl.conf'  peer_ip = '203.0.113.45'  connection_name = 'main-branch' +local_id = 'left' +remote_id = 'right'  interface = 'eth1'  vif = '100'  esp_group = 'MyESPGroup'  ike_group = 'MyIKEGroup'  secret = 'MYSECRETKEY'  PROCESS_NAME = 'charon' +regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}'  ca_pem = """  MIIDSzCCAjOgAwIBAgIUQHK+ZgTUYZksvXY2/MyW+Jiels4wDQYJKoZIhvcNAQEL @@ -151,10 +154,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):          # 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}']) @@ -172,18 +180,25 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):      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]) @@ -230,12 +245,14 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):              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-{regex_uuid4} = "{local_id}"', +            f'id-{regex_uuid4} = "{remote_id}"', +            f'id-{regex_uuid4} = "{local_address}"', +            f'id-{regex_uuid4} = "{peer_ip}"',              f'secret = "{secret}"'          ]          for line in swanctl_secrets_lines: -            self.assertIn(line, swanctl_conf) +            self.assertRegex(swanctl_conf, fr'{line}')      def test_03_site_to_site_vti(self): @@ -249,10 +266,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):          # 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]) @@ -295,12 +317,12 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):              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-{regex_uuid4} = "{local_id}"', +            f'id-{regex_uuid4} = "{remote_id}"',              f'secret = "{secret}"'          ]          for line in swanctl_secrets_lines: -            self.assertIn(line, swanctl_conf) +            self.assertRegex(swanctl_conf, fr'{line}')      def test_04_dmvpn(self): @@ -454,9 +476,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):          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]) @@ -486,15 +514,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):              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-{regex_uuid4} = "{local_id}"', +            f'id-{regex_uuid4} = "{remote_id}"', +            f'id-{regex_uuid4} = "{peer_ip}"', +            f'id-{regex_uuid4} = "{local_address}"',              f'secret = "{secret}"',          ]          for line in swanctl_secrets_lines: -            self.assertIn(line, swanctl_conf) +            self.assertRegex(swanctl_conf, fr'{line}')          # Verify charon configuration          charon_conf = read_file(charon_file) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 3af2af4d9..ce4f13d27 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/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 @@ -17,6 +17,7 @@  import ipaddress  import os  import re +import jmespath  from sys import exit  from time import sleep @@ -219,6 +220,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) @@ -602,6 +609,14 @@ def generate(ipsec):                      ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough +        # auth psk <tag> dhcp-interface <xxx> +        if jmespath.search('authentication.psk.*.dhcp_interface', ipsec): +            for psk, psk_config in ipsec['authentication']['psk'].items(): +                if 'dhcp_interface' in psk_config: +                    for iface in psk_config['dhcp_interface']: +                        id = get_dhcp_address(iface) +                        if id: +                            ipsec['authentication']['psk'][psk]['id'].append(id)      render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec)      render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec) diff --git a/src/migration-scripts/ipsec/10-to-11 b/src/migration-scripts/ipsec/10-to-11 new file mode 100755 index 000000000..ec38d0034 --- /dev/null +++ b/src/migration-scripts/ipsec/10-to-11 @@ -0,0 +1,85 @@ +#!/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) +            # format as tag node to avoid loading problems +            config.set_tag(base + ['authentication', 'psk']) + +            # 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']) +                if tmp: +                    for remote_addr in tmp: +                        if remote_addr == 'any': +                            remote_addr = '%any' +                        config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_addr, replace=False) + +            # get DHCP peer interface as psk dhcp-interface +            if config.exists(peer_base + ['dhcp-interface']): +                tmp = config.return_value(peer_base + ['dhcp-interface']) +                config.set(base + ['authentication', 'psk', peer, 'dhcp-interface'], value=tmp) + + +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) | 
