diff --git a/.zuul.yaml b/.zuul.yaml index aef884c10..af0839f73 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -4,6 +4,7 @@ - neutron-vpnaas-dsvm-functional-sswan - neutron-vpnaas-tempest - openstack-tox-lower-constraints + - neutron-vpnaas-tempest-libreswan-centos gate: jobs: - neutron-vpnaas-dsvm-functional-sswan @@ -35,6 +36,22 @@ - ^neutron_vpnaas/tests/unit/.*$ - ^releasenotes/.*$ +- job: + name: neutron-vpnaas-tempest-libreswan-centos + parent: neutron-vpnaas-tempest + nodeset: devstack-single-node-centos-7 + vars: + devstack_localrc: + IPSEC_PACKAGE: libreswan + # VPNaaS 4in6 and 6in4 scenarios would fail after LibreSwan 3.18. + # Base node using Libreswan 3.20 on CentOS 7.4. + # Refer to https://github.com/libreswan/libreswan/issues/175. + devstack_local_conf: + test-config: + $TEMPEST_CONFIG: + neutron_vpnaas_plugin_options: + skip_4in6_6in4_tests: true + - job: name: neutron-vpnaas-dsvm-functional-sswan parent: legacy-dsvm-base diff --git a/etc/neutron/rootwrap.d/vpnaas.filters b/etc/neutron/rootwrap.d/vpnaas.filters index e8a3e0042..846ac2d1c 100644 --- a/etc/neutron/rootwrap.d/vpnaas.filters +++ b/etc/neutron/rootwrap.d/vpnaas.filters @@ -17,4 +17,4 @@ rm_file: RegExpFilter, rm, root, rm, -f, .*/ipsec.secrets strongswan: CommandFilter, strongswan, root neutron_netns_wrapper: CommandFilter, neutron-vpn-netns-wrapper, root neutron_netns_wrapper_local: CommandFilter, /usr/local/bin/neutron-vpn-netns-wrapper, root -chown: RegExpFilter, chown, root, chown, --from=.*, root.root, .*/ipsec.secrets +chown: RegExpFilter, chown, root, chown, --from=.*, root.root, .*/(ipsec.secrets|ipsec/[0-9a-z-]+/log) diff --git a/neutron_vpnaas/services/vpn/device_drivers/ipsec.py b/neutron_vpnaas/services/vpn/device_drivers/ipsec.py index 8a2b862ea..dc8cf9ae0 100644 --- a/neutron_vpnaas/services/vpn/device_drivers/ipsec.py +++ b/neutron_vpnaas/services/vpn/device_drivers/ipsec.py @@ -602,7 +602,7 @@ class OpenSwanProcess(BaseSwanProcess): return routes.split(' ')[2] return address - def _virtual_privates(self): + def _virtual_privates(self, vpnservice): """Returns line of virtual_privates. virtual_private contains the networks @@ -610,14 +610,77 @@ class OpenSwanProcess(BaseSwanProcess): """ virtual_privates = [] nets = [] - for ipsec_site_conn in self.vpnservice['ipsec_site_connections']: + for ipsec_site_conn in vpnservice['ipsec_site_connections']: nets += ipsec_site_conn['local_cidrs'] nets += ipsec_site_conn['peer_cidrs'] for net in nets: version = netaddr.IPNetwork(net).version virtual_privates.append('%%v%s:%s' % (version, net)) + virtual_privates.sort() return ','.join(virtual_privates) + def _gen_config_content(self, template_file, vpnservice): + template = _get_template(template_file) + virtual_privates = self._virtual_privates(vpnservice) + return template.render( + {'vpnservice': vpnservice, + 'virtual_privates': virtual_privates}) + + def start_pluto(self): + cmd = [self.binary, + 'pluto', + '--ctlbase', self.pid_path, + '--ipsecdir', self.etc_dir, + '--use-netkey', + '--uniqueids', + '--nat_traversal', + '--secretsfile', self.secrets_file] + + if self.conf.ipsec.enable_detailed_logging: + cmd += ['--perpeerlog', '--perpeerlogbase', self.log_dir] + self._execute(cmd) + + def add_ipsec_connection(self, nexthop, conn_id): + self._execute([self.binary, + 'addconn', + '--ctlbase', '%s.ctl' % self.pid_path, + '--defaultroutenexthop', nexthop, + '--config', self.config_file, conn_id + ]) + + def start_whack_listening(self): + #TODO(nati) fix this when openswan is fixed + #Due to openswan bug, this command always exit with 3 + self._execute([self.binary, + 'whack', + '--ctlbase', self.pid_path, + '--listen' + ], check_exit_code=False) + + def shutdown_whack(self): + self._execute([self.binary, + 'whack', + '--ctlbase', self.pid_path, + '--shutdown' + ]) + + def initiate_connection(self, conn_name): + self._execute([self.binary, + 'whack', + '--ctlbase', self.pid_path, + '--name', conn_name, + '--asynchronous', + '--initiate' + ]) + + def terminate_connection(self, conn_name): + self._execute([self.binary, + 'whack', + '--ctlbase', self.pid_path, + '--name', conn_name, + '--terminate' + ]) + def start(self): """Start the process. @@ -642,21 +705,9 @@ class OpenSwanProcess(BaseSwanProcess): if not self._process_running(): self._cleanup_control_files() - virtual_private = self._virtual_privates() #start pluto IKE keying daemon - cmd = [self.binary, - 'pluto', - '--ctlbase', self.pid_path, - '--ipsecdir', self.etc_dir, - '--use-netkey', - '--uniqueids', - '--nat_traversal', - '--secretsfile', self.secrets_file, - '--virtual_private', virtual_private] + self.start_pluto() - if self.conf.ipsec.enable_detailed_logging: - cmd += ['--perpeerlog', '--perpeerlogbase', self.log_dir] - self._execute(cmd) #add connections for ipsec_site_conn in self.vpnservice['ipsec_site_connections']: # Don't add a connection if its admin state is down @@ -664,34 +715,17 @@ class OpenSwanProcess(BaseSwanProcess): continue nexthop = self._get_nexthop(ipsec_site_conn['peer_address'], ipsec_site_conn['id']) - self._execute([self.binary, - 'addconn', - '--ctlbase', '%s.ctl' % self.pid_path, - '--defaultroutenexthop', nexthop, - '--config', self.config_file, - ipsec_site_conn['id'] - ]) - #TODO(nati) fix this when openswan is fixed - #Due to openswan bug, this command always exit with 3 + self.add_ipsec_connection(nexthop, ipsec_site_conn['id']) + #start whack ipsec keying daemon - self._execute([self.binary, - 'whack', - '--ctlbase', self.pid_path, - '--listen', - ], check_exit_code=False) + self.start_whack_listening() for ipsec_site_conn in self.vpnservice['ipsec_site_connections']: if (not ipsec_site_conn['initiator'] == 'start' or not ipsec_site_conn['admin_state_up']): continue #initiate ipsec connection - self._execute([self.binary, - 'whack', - '--ctlbase', self.pid_path, - '--name', ipsec_site_conn['id'], - '--asynchronous', - '--initiate' - ]) + self.initiate_connection(ipsec_site_conn['id']) self._copy_configs() def get_established_connections(self): @@ -720,22 +754,13 @@ class OpenSwanProcess(BaseSwanProcess): connections = self.get_established_connections() for conn_name in connections: - self._execute([self.binary, - 'whack', - '--ctlbase', self.pid_path, - '--name', '%s' % conn_name, - '--terminate' - ]) + self.terminate_connection(conn_name) def stop(self): #Stop process using whack #Note this will also stop pluto self.disconnect() - self._execute([self.binary, - 'whack', - '--ctlbase', self.pid_path, - '--shutdown', - ]) + self.shutdown_whack() self.connection_status = {} diff --git a/neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py b/neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py index 94153136c..680150c28 100644 --- a/neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py +++ b/neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py @@ -15,8 +15,12 @@ import os import os.path +from neutron.agent.linux import ip_lib + from neutron_vpnaas.services.vpn.device_drivers import ipsec +NS_WRAPPER = 'neutron-vpn-netns-wrapper' + class LibreSwanProcess(ipsec.OpenSwanProcess): """Libreswan Process manager class. @@ -27,6 +31,33 @@ class LibreSwanProcess(ipsec.OpenSwanProcess): super(LibreSwanProcess, self).__init__(conf, process_id, vpnservice, namespace) + def _ipsec_execute(self, cmd, check_exit_code=True, extra_ok_codes=None): + """Execute ipsec command on namespace. + + This execute is wrapped by namespace wrapper. + The namespace wrapper will bind /etc and /var/run + """ + ip_wrapper = ip_lib.IPWrapper(namespace=self.namespace) + mount_paths = {'/etc': '%s/etc' % self.config_dir, + '/var/run': '%s/var/run' % self.config_dir} + mount_paths_str = ','.join( + "%s:%s" % (source, target) + for source, target in mount_paths.items()) + return ip_wrapper.netns.execute( + [NS_WRAPPER, + '--mount_paths=%s' % mount_paths_str, + '--cmd=%s,%s' % (self.binary, ','.join(cmd))], + check_exit_code=check_exit_code, + extra_ok_codes=extra_ok_codes) + + def _ensure_needed_files(self): + # addconn reads from /etc/hosts and /etc/resolv.conf. As /etc would be + # bind-mounted, create these two empty files in the target directory. + with open('%s/etc/hosts' % self.config_dir, 'a'): + pass + with open('%s/etc/resolv.conf' % self.config_dir, 'a'): + pass + def ensure_configs(self): """Generate config files which are needed for Libreswan. @@ -50,15 +81,54 @@ class LibreSwanProcess(ipsec.OpenSwanProcess): self._execute(['chown', '--from=%s' % os.getuid(), 'root:root', secrets_file]) + # Libreswan needs to write logs to this directory. + self._execute(['chown', '--from=%s' % os.getuid(), 'root:root', + self.log_dir]) + + self._ensure_needed_files() + # Load the ipsec kernel module if not loaded - self._execute([self.binary, '_stackmanager', 'start']) + self._ipsec_execute(['_stackmanager', 'start']) # checknss creates nssdb only if it is missing # It is added in Libreswan version v3.10 # For prior versions use initnss try: - self._execute([self.binary, 'checknss', self.etc_dir]) + self._ipsec_execute(['checknss']) except RuntimeError: - self._execute([self.binary, 'initnss', self.etc_dir]) + self._ipsec_execute(['initnss']) + + def get_status(self): + return self._ipsec_execute(['whack', '--status'], + extra_ok_codes=[1, 3]) + + def start_pluto(self): + cmd = ['pluto', + '--use-netkey', + '--uniqueids'] + + if self.conf.ipsec.enable_detailed_logging: + cmd += ['--perpeerlog', '--perpeerlogbase', self.log_dir] + self._ipsec_execute(cmd) + + def add_ipsec_connection(self, nexthop, conn_id): + # Connections will be automatically added as auto=start/add for + # initiator=bi-directional/response-only specified in the config. + pass + + def start_whack_listening(self): + # NOTE(huntxu): This is a workaround for with a weak (len<8) secret, + # "ipsec whack --listen" will exit with 3. + self._ipsec_execute(['whack', '--listen'], extra_ok_codes=[3]) + + def shutdown_whack(self): + self._ipsec_execute(['whack', '--shutdown']) + + def initiate_connection(self, conn_name): + self._ipsec_execute( + ['whack', '--name', conn_name, '--asynchronous', '--initiate']) + + def terminate_connection(self, conn_name): + self._ipsec_execute(['whack', '--name', conn_name, '--terminate']) class LibreSwanDriver(ipsec.IPsecDriver): diff --git a/neutron_vpnaas/services/vpn/device_drivers/template/openswan/ipsec.conf.template b/neutron_vpnaas/services/vpn/device_drivers/template/openswan/ipsec.conf.template index d18cf4ffb..fa64fb175 100644 --- a/neutron_vpnaas/services/vpn/device_drivers/template/openswan/ipsec.conf.template +++ b/neutron_vpnaas/services/vpn/device_drivers/template/openswan/ipsec.conf.template @@ -1,6 +1,7 @@ # Configuration for {{vpnservice.id}} config setup nat_traversal=yes + virtual_private={{virtual_privates}} conn %default keylife=60m keyingtries=%forever @@ -69,7 +70,7 @@ conn {{ipsec_site_connection.id}} # IPsecPolicys params ########################## # [transform_protocol] - auth={{ipsec_site_connection.ipsecpolicy.transform_protocol}} + phase2={{ipsec_site_connection.ipsecpolicy.transform_protocol}} {% if ipsec_site_connection.ipsecpolicy.transform_protocol == "ah" -%} # AH protocol does not support encryption # [auth_algorithm]-[pfs] diff --git a/neutron_vpnaas/tests/tempest/scenario/test_vpnaas.py b/neutron_vpnaas/tests/tempest/scenario/test_vpnaas.py index 3cfd0aae6..2a777761e 100644 --- a/neutron_vpnaas/tests/tempest/scenario/test_vpnaas.py +++ b/neutron_vpnaas/tests/tempest/scenario/test_vpnaas.py @@ -14,6 +14,7 @@ # under the License. import netaddr +from oslo_config import cfg import testtools from tempest.common import utils @@ -30,6 +31,23 @@ from neutron_vpnaas.tests.tempest.scenario import base CONF = config.CONF +# NOTE(huntxu): This is a workaround due to a upstream bug [1]. +# VPNaaS 4in6 and 6in4 is not working properly with LibreSwan 3.19+. +# In OpenStack zuul checks the base CentOS 7 node is using Libreswan 3.20 on +# CentOS 7.4. So we need to provide a way to skip the 4in6 and 6in4 test cases +# for zuul. +# +# Once the upstream bug gets fixed and the base node uses a newer version of +# Libreswan with that fix, we can remove this. +# +# [1] https://github.com/libreswan/libreswan/issues/175 +CONF.register_opt( + cfg.BoolOpt('skip_4in6_6in4_tests', + default=False, + help='Whether to skip 4in6 and 6in4 test cases.'), + 'neutron_vpnaas_plugin_options' +) + class Vpnaas(base.BaseTempestTestCase): """Test the following topology @@ -247,6 +265,9 @@ class Vpnaas4in6(Vpnaas): @decorators.idempotent_id('2d5f18dc-6186-4deb-842b-051325bd0466') @testtools.skipUnless(CONF.network_feature_enabled.ipv6, 'IPv6 tests are disabled.') + @testtools.skipIf( + CONF.neutron_vpnaas_plugin_options.skip_4in6_6in4_tests, + 'VPNaaS 4in6 test is skipped.') def test_vpnaas_4in6(self): self._test_vpnaas() @@ -257,6 +278,9 @@ class Vpnaas6in4(Vpnaas): @decorators.idempotent_id('10febf33-c5b7-48af-aa13-94b4fb585a55') @testtools.skipUnless(CONF.network_feature_enabled.ipv6, 'IPv6 tests are disabled.') + @testtools.skipIf( + CONF.neutron_vpnaas_plugin_options.skip_4in6_6in4_tests, + 'VPNaaS 6in4 test is skipped.') def test_vpnaas_6in4(self): self._test_vpnaas() diff --git a/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py b/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py index 221011625..19f31e7d1 100644 --- a/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py +++ b/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py @@ -19,6 +19,7 @@ import os import socket import mock +import netaddr from neutron.agent.l3 import dvr_edge_router from neutron.agent.l3 import dvr_snat_ns from neutron.agent.l3 import legacy_router @@ -138,7 +139,7 @@ OPENSWAN_CONNECTION_DETAILS = '''# rightsubnet=networkA/netmaskA, networkB/netma # IPsecPolicys params ########################## # [transform_protocol] - auth=%(auth_mode)s + phase2=%(auth_mode)s # [encapsulation_mode] type=%(encapsulation_mode)s # [lifetime_value] @@ -162,6 +163,7 @@ EXPECTED_OPENSWAN_CONF = """ # Configuration for %(vpnservice_id)s config setup nat_traversal=yes + virtual_private=%(virtual_privates)s conn %%default keylife=60m keyingtries=%%forever @@ -959,6 +961,7 @@ class TestOpenSwanConfigGeneration(BaseIPsecDeviceDriver): 'ike_lifetime': 3600, 'life_time': 3600, 'encapsulation_mode': 'tunnel'} + virtual_privates = [] # Convert local CIDRs into assignment strings. IF more than one, # pluralize the attribute name and enclose in brackets. cidrs = info.get('local_cidrs', [['10.0.0.0/24'], ['11.0.0.0/24']]) @@ -968,16 +971,25 @@ class TestOpenSwanConfigGeneration(BaseIPsecDeviceDriver): local_cidrs.append("s={ %s }" % ' '.join(cidr)) else: local_cidrs.append("=%s" % cidr[0]) + for net in cidr: + version = netaddr.IPNetwork(net).version + virtual_privates.append('%%v%s:%s' % (version, net)) # Convert peer CIDRs into space separated strings cidrs = info.get('peer_cidrs', [['20.0.0.0/24', '30.0.0.0/24'], ['40.0.0.0/24', '50.0.0.0/24']]) + for cidr in cidrs: + for net in cidr: + version = netaddr.IPNetwork(net).version + virtual_privates.append('%%v%s:%s' % (version, net)) peer_cidrs = [' '.join(cidr) for cidr in cidrs] local_ip = info.get('local', '60.0.0.4') version = info.get('local_ip_vers', 4) next_hop = IPV4_NEXT_HOP if version == 4 else IPV6_NEXT_HOP % local_ip peer_ips = info.get('peers', ['60.0.0.5', '60.0.0.6']) + virtual_privates.sort() return EXPECTED_OPENSWAN_CONF % { 'vpnservice_id': FAKE_VPNSERVICE_ID, + 'virtual_privates': ','.join(virtual_privates), 'next_hop': next_hop, 'local_cidrs1': local_cidrs[0], 'local_cidrs2': local_cidrs[1], 'local_ver': version, @@ -1412,8 +1424,15 @@ class TestLibreSwanProcess(base.BaseTestCase): @mock.patch('os.path.exists', return_value=True) def test_ensure_configs_on_restart(self, exists_mock): openswan_ipsec.OpenSwanProcess.ensure_configs = mock.Mock() - with mock.patch.object(self.ipsec_process, '_execute') as fake_execute: + with mock.patch.object( + self.ipsec_process, '_execute' + ) as fake_execute, mock.patch.object( + self.ipsec_process, '_ipsec_execute' + ) as fake_ipsec_execute, mock.patch.object( + self.ipsec_process, '_ensure_needed_files' + ) as fake_ensure_needed_files: self.ipsec_process.ensure_configs() + expected = [mock.call(['rm', '-f', self.ipsec_process._get_config_filename( 'ipsec.secrets')]), @@ -1421,45 +1440,76 @@ class TestLibreSwanProcess(base.BaseTestCase): 'root:root', self.ipsec_process._get_config_filename( 'ipsec.secrets')]), - mock.call(['ipsec', '_stackmanager', 'start']), - mock.call(['ipsec', 'checknss', - self.ipsec_process.etc_dir])] + mock.call(['chown', '--from=%s' % os.getuid(), + 'root:root', self.ipsec_process.log_dir])] fake_execute.assert_has_calls(expected) - self.assertEqual(4, fake_execute.call_count) + self.assertEqual(3, fake_execute.call_count) + + expected = [mock.call(['_stackmanager', 'start']), + mock.call(['checknss'])] + fake_ipsec_execute.assert_has_calls(expected) + self.assertEqual(2, fake_ipsec_execute.call_count) + + self.assertTrue(fake_ensure_needed_files.called) self.assertTrue(exists_mock.called) @mock.patch('os.path.exists', return_value=False) def test_ensure_configs(self, exists_mock): openswan_ipsec.OpenSwanProcess.ensure_configs = mock.Mock() - with mock.patch.object(self.ipsec_process, '_execute') as fake_execute: + with mock.patch.object( + self.ipsec_process, '_execute' + ) as fake_execute, mock.patch.object( + self.ipsec_process, '_ipsec_execute' + ) as fake_ipsec_execute, mock.patch.object( + self.ipsec_process, '_ensure_needed_files' + ) as fake_ensure_needed_files: self.ipsec_process.ensure_configs() + expected = [mock.call(['chown', '--from=%s' % os.getuid(), 'root:root', self.ipsec_process._get_config_filename( 'ipsec.secrets')]), - mock.call(['ipsec', '_stackmanager', 'start']), - mock.call(['ipsec', 'checknss', - self.ipsec_process.etc_dir])] + mock.call(['chown', '--from=%s' % os.getuid(), + 'root:root', self.ipsec_process.log_dir])] fake_execute.assert_has_calls(expected) - self.assertEqual(3, fake_execute.call_count) + self.assertEqual(2, fake_execute.call_count) + + expected = [mock.call(['_stackmanager', 'start']), + mock.call(['checknss'])] + fake_ipsec_execute.assert_has_calls(expected) + self.assertEqual(2, fake_ipsec_execute.call_count) + + self.assertTrue(fake_ensure_needed_files.called) self.assertTrue(exists_mock.called) exists_mock.reset_mock() - with mock.patch.object(self.ipsec_process, '_execute') as fake_execute: - fake_execute.side_effect = [None, None, RuntimeError, None] + with mock.patch.object( + self.ipsec_process, '_execute' + ) as fake_execute, mock.patch.object( + self.ipsec_process, '_ipsec_execute' + ) as fake_ipsec_execute, mock.patch.object( + self.ipsec_process, '_ensure_needed_files' + ) as fake_ensure_needed_files: + fake_ipsec_execute.side_effect = [None, RuntimeError, None] self.ipsec_process.ensure_configs() + expected = [mock.call(['chown', '--from=%s' % os.getuid(), 'root:root', self.ipsec_process._get_config_filename( 'ipsec.secrets')]), - mock.call(['ipsec', '_stackmanager', 'start']), - mock.call(['ipsec', 'checknss', - self.ipsec_process.etc_dir]), - mock.call(['ipsec', 'initnss', - self.ipsec_process.etc_dir])] + mock.call(['chown', '--from=%s' % os.getuid(), + 'root:root', self.ipsec_process.log_dir])] fake_execute.assert_has_calls(expected) - self.assertEqual(4, fake_execute.call_count) + self.assertEqual(2, fake_execute.call_count) + + expected = [mock.call(['_stackmanager', 'start']), + mock.call(['checknss']), + mock.call(['initnss'])] + self.assertEqual(3, fake_ipsec_execute.call_count) + fake_ipsec_execute.assert_has_calls(expected) + + self.assertTrue(fake_ensure_needed_files.called) self.assertTrue(exists_mock.called) diff --git a/releasenotes/notes/libreswan-driver-works-with-3.19+-7e1fc79ac6c7efe5.yaml b/releasenotes/notes/libreswan-driver-works-with-3.19+-7e1fc79ac6c7efe5.yaml new file mode 100644 index 000000000..57a539641 --- /dev/null +++ b/releasenotes/notes/libreswan-driver-works-with-3.19+-7e1fc79ac6c7efe5.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - The libreswan driver of neutron-vpnaas can now also work with Libreswan + 3.19+ (bug `#1711456 `_).