Make libreswan driver work with recent versions

LibreSwan 3.19 introduces a new commandline argument '--nssdir' for
pluto which defaults to '/etc/ipsec.d'. As older versions don't
understand such an option, we cannot just add it to the commandline.

The commandline arguments of LibreSwan are not stable enough to rely on.
For example, in 3.19, 'ipsec initnss' has the new argument '--nssdir',
and in 3.20, 'ipsec pluto' also gets this new argument '--nssdir', then
in 3.22, the argument '--ctlbase' is phased out.

In this commit, instead of trying new options and then fallback to old
ones for older versions, the bind-mount method used in StrongSwan driver
is adopted. With /etc and /var/run bind mounted, all the commandline
arguments related to configuration file places can be removed. This
ensures that changes of such arguments between different versions won't
bother as the default places are always used.

This commit also replaces 'auth=' by 'phase2=' in the configuration
template as the former is for a long time an alias of the latter and
removed in LibreSwan 3.19.

The virtual-private argument of 'ipsec pluto' has been put into the
configuration file to avoid commas(,) in the commandline so that the
netns_wrapper can work well.

A new tempest job for running LibreSwan as the device driver on CentOS 7
is also added to avoid regression.

This commit has been simply tested on CentOS 7.4 with the following
versions of LibreSwan provided by the CentOS repo:

  - libreswan-3.12-5.el7.x86_64.rpm
  - libreswan-3.12-10.1.el7_1.x86_64.rpm
  - libreswan-3.15-5.el7_1.x86_64.rpm
  - libreswan-3.15-8.el7.x86_64.rpm
  - libreswan-3.20-3.el7.x86_64.rpm
  - libreswan-3.20-5.el7_4.x86_64.rpm

and different versions of LibreSwan provided by libreswan.org[1]:

[1] https://download.libreswan.org/binaries/rhel/7/x86_64/

Change-Id: Iacb6f13187b49cf771f0c24662d6af9217c211b8
Closes-Bug: #1711456
This commit is contained in:
Hunt Xu 2018-03-20 17:52:55 +08:00
parent 0184b7d630
commit b6c8ea8a3c
8 changed files with 262 additions and 71 deletions

View File

@ -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

View File

@ -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)

View File

@ -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 = {}

View File

@ -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):

View File

@ -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]

View File

@ -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()

View File

@ -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)

View File

@ -0,0 +1,4 @@
---
fixes:
- The libreswan driver of neutron-vpnaas can now also work with Libreswan
3.19+ (bug `#1711456 <https://launchpad.net/bugs/1711456>`_).