IPsec strongSwan driver implemention

This patch implements strongSwan driver for VPNaaS, Initial effort
only supports strongSwan >= 5.x and implements an equivalent psk
net-to-net vpn service as openSwan driver did based on recommended
configuration.

Implements blueprint ipsec-strongswan-driver

DocImpact
a, StrongSwan and openSwan cannot be installed at the same time.
   Thus, both cannot be enabled for use. In the future when
   flavors/STF support is available, this will still constrain
   the flavors which can be used together.
b, Adding StrongswanDriver in the file /etc/neutron/vpn_agent.ini
   vpn_device_driver=neutron.services.vpn.device_drivers \
       .strongswan_ipsec.StrongSwanDriver
c, Apparmor can interfere with both creation of tunnels and
   established tunnels, because it controls access to operating
   system resources. Thus we can use below commands to remove
   apparmor definitions from the kernel.
     sudo apparmor_parser -R usr.lib.ipsec.charon
     sudo apparmor_parser -R usr.lib.ipsec.stroke
   However, what we are seeing is that even though we disable the
   profile for charon and stroke, apparmor is re-enabling them
   (at unknown times). As a result, we can directly disable them
   by the following commands:
     sudo ln -s /etc/apparmor.d/usr.lib.ipsec.charon \
         /etc/apparmor.d/disable/
     sudo ln -s /etc/apparmor.d/usr.lib.ipsec.stroke \
         /etc/apparmor.d/disable/
     sudo service apparmor restart
   In a word, you need to take care of your apparmor configurations.
d, Supports Ubuntu only. A separate commit will address Fedora.
e, Other steps are the same with the existing openSwan driver.

This patch is based on patchset67 of nachi's initial vpnaas
implementation, many thanks to nachi.

Change-Id: Ia3eb10a9103bdceb2a29f2372b410b946f3a89cc
This commit is contained in:
Zhang Hua 2015-03-11 01:45:56 +08:00
parent 810bcf53e7
commit 6d55e377ea
9 changed files with 272 additions and 29 deletions

View File

@ -10,6 +10,6 @@
ip: IpFilter, ip, root
ip_exec: IpNetnsExecFilter, ip, root
openswan: CommandFilter, ipsec, root
ipsec: CommandFilter, ipsec, root
neutron_netns_wrapper: CommandFilter, neutron-vpn-netns-wrapper, root
neutron_netns_wrapper_local: CommandFilter, /usr/local/bin/neutron-vpn-netns-wrapper, root

View File

@ -5,11 +5,21 @@
[vpnagent]
# vpn device drivers which vpn agent will use
# If we want to use multiple drivers, we need to define this option multiple times.
# vpn_device_driver=neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver
# vpn_device_driver=neutron.services.vpn.device_drivers.cisco_ipsec.CiscoCsrIPsecDriver
# vpn_device_driver=neutron.services.vpn.device_drivers.vyatta_ipsec.VyattaIPSecDriver
# NOTE: StrongSwan and openSwan cannot be installed at the same time. Thus, both cannot
# be enabled for use. In the future when flavors/STF support is available,
# this will still constrain the flavors which can be used together.
# vpn_device_driver=neutron_vpnaas.services.vpn.device_drivers.ipsec.OpenSwanDriver
# vpn_device_driver=neutron_vpnaas.services.vpn.device_drivers.cisco_ipsec.CiscoCsrIPsecDriver
# vpn_device_driver=neutron_vpnaas.services.vpn.device_drivers.vyatta_ipsec.VyattaIPSecDriver
# vpn_device_driver=neutron_vpnaas.services.vpn.device_drivers.strongswan_ipsec.StrongSwanDriver
# vpn_device_driver=another_driver
[ipsec]
# Status check interval
# ipsec_status_check_interval=60
[strongswan]
# For fedora use:
# default_config_area=/usr/share/strongswan/templates/config/strongswan.d
# Default is for ubuntu use, /etc/strongswan.d
# default_config_area=/etc/strongswan.d

View File

@ -61,13 +61,17 @@ def execute(cmd):
env=env)
_stdout, _stderr = obj.communicate()
LOG.debug('Command: %(cmd)s Exit code: %(returncode)s '
'Stdout: %(stdout)s Stderr: %(stderr)s',
{'cmd': cmd,
'returncode': obj.returncode,
'stdout': _stdout,
'stderr': _stderr})
msg = ('Command: %(cmd)s Exit code: %(returncode)s '
'Stdout: %(stdout)s Stderr: %(stderr)s' %
{'cmd': cmd,
'returncode': obj.returncode,
'stdout': _stdout,
'stderr': _stderr})
LOG.debug(msg)
obj.stdin.close()
# Pass the output to calling process
sys.stdout.write(msg)
sys.stdout.flush()
return obj.returncode
@ -117,7 +121,7 @@ def execute_with_mount():
# Both sudoers and rootwrap.conf will not exist in the directory /etc
# after bind-mount, so we can't use utils.execute(conf.cmd,
# conf.root_helper). That's why we have to check here if cmd matches
# run_as_root=True). That's why we have to check here if cmd matches
# CommandFilter
filter_command(conf.cmd, conf.rootwrap_config)

View File

@ -42,7 +42,7 @@ from neutron_vpnaas.services.vpn.common import topics
from neutron_vpnaas.services.vpn import device_drivers
LOG = logging.getLogger(__name__)
TEMPLATE_PATH = os.path.dirname(__file__)
TEMPLATE_PATH = os.path.dirname(os.path.abspath(__file__))
ipsec_opts = [
cfg.StrOpt(
@ -74,11 +74,6 @@ cfg.CONF.register_opts(openswan_opts, 'openswan')
JINJA_ENV = None
STATUS_MAP = {
'erouted': constants.ACTIVE,
'unrouted': constants.DOWN
}
IPSEC_CONNS = 'ipsec_site_connections'
@ -130,6 +125,13 @@ class BaseSwanProcess(object):
"v1": "never"
}
STATUS_DICT = {
'erouted': constants.ACTIVE,
'unrouted': constants.DOWN
}
STATUS_RE = '\d\d\d "([a-f0-9\-]+).* (unrouted|erouted);'
STATUS_NOT_RUNNING_RE = 'Command:.*ipsec.*status.*Exit code: [1|3]$'
def __init__(self, conf, process_id, vpnservice, namespace):
self.conf = conf
self.id = process_id
@ -140,6 +142,10 @@ class BaseSwanProcess(object):
cfg.CONF.ipsec.config_base_dir, self.id)
self.etc_dir = os.path.join(self.config_dir, 'etc')
self.update_vpnservice(vpnservice)
self.STATUS_PATTERN = re.compile(self.STATUS_RE)
self.STATUS_NOT_RUNNING_PATTERN = re.compile(
self.STATUS_NOT_RUNNING_RE)
self.STATUS_MAP = self.STATUS_DICT
def translate_dialect(self):
if not self.vpnservice:
@ -213,6 +219,8 @@ class BaseSwanProcess(object):
try:
status = self.get_status()
self._update_connection_status(status)
if not self.connection_status:
return False
except RuntimeError:
return False
return True
@ -274,8 +282,14 @@ class BaseSwanProcess(object):
"""Stop process."""
def _update_connection_status(self, status_output):
if not status_output:
self.connection_status = {}
return
for line in status_output.split('\n'):
m = re.search('\d\d\d "([a-f0-9\-]+).* (unrouted|erouted);', line)
if self.STATUS_NOT_RUNNING_PATTERN.search(line):
self.connection_status = {}
break
m = self.STATUS_PATTERN.search(line)
if not m:
continue
connection_id = m.group(1)
@ -286,7 +300,7 @@ class BaseSwanProcess(object):
'updated_pending_status': False
}
self.connection_status[
connection_id]['status'] = STATUS_MAP[status]
connection_id]['status'] = self.STATUS_MAP[status]
class OpenSwanProcess(BaseSwanProcess):
@ -307,10 +321,11 @@ class OpenSwanProcess(BaseSwanProcess):
self.pid_path = os.path.join(
self.config_dir, 'var', 'run', 'pluto')
def _execute(self, cmd, check_exit_code=True):
def _execute(self, cmd, check_exit_code=True, extra_ok_codes=None):
"""Execute command on namespace."""
ip_wrapper = ip_lib.IPWrapper(namespace=self.namespace)
return ip_wrapper.netns.execute(cmd, check_exit_code=check_exit_code)
return ip_wrapper.netns.execute(cmd, check_exit_code=check_exit_code,
extra_ok_codes=extra_ok_codes)
def ensure_configs(self):
"""Generate config files which are needed for OpenSwan.
@ -333,7 +348,7 @@ class OpenSwanProcess(BaseSwanProcess):
'whack',
'--ctlbase',
self.pid_path,
'--status'])
'--status'], extra_ok_codes=[1, 3])
def restart(self):
"""Restart the process."""

View File

@ -0,0 +1,163 @@
# Copyright (c) 2015 Canonical, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import shutil
from oslo_config import cfg
from oslo_log import log as logging
from neutron.agent.linux import ip_lib
from neutron.plugins.common import constants
from neutron_vpnaas.services.vpn.device_drivers import ipsec
LOG = logging.getLogger(__name__)
TEMPLATE_PATH = os.path.dirname(os.path.abspath(__file__))
strongswan_opts = [
cfg.StrOpt(
'ipsec_config_template',
default=os.path.join(
TEMPLATE_PATH,
'template/strongswan/ipsec.conf.template'),
help=_('Template file for ipsec configuration.')),
cfg.StrOpt(
'strongswan_config_template',
default=os.path.join(
TEMPLATE_PATH,
'template/strongswan/strongswan.conf.template'),
help=_('Template file for strongswan configuration.')),
cfg.StrOpt(
'ipsec_secret_template',
default=os.path.join(
TEMPLATE_PATH,
'template/strongswan/ipsec.secret.template'),
help=_('Template file for ipsec secret configuration.')),
cfg.StrOpt(
'default_config_area',
default=os.path.join(
TEMPLATE_PATH,
'/etc/strongswan.d'),
help=_('The area where default StrongSwan configuration '
'files are located.'))
]
cfg.CONF.register_opts(strongswan_opts, 'strongswan')
NS_WRAPPER = 'neutron-vpn-netns-wrapper'
class StrongSwanProcess(ipsec.BaseSwanProcess):
# ROUTED means route created. (only for auto=route mode)
# CONNECTING means route created, connection tunnel is negotiating.
# INSTALLED means route created,
# also connection tunnel installed. (traffic can pass)
STATUS_DICT = {
'ROUTED': constants.DOWN,
'CONNECTING': constants.DOWN,
'INSTALLED': constants.ACTIVE
}
STATUS_RE = '([a-f0-9\-]+).* (ROUTED|CONNECTING|INSTALLED)'
STATUS_NOT_RUNNING_RE = 'Command:.*ipsec.*status.*Exit code: [1|3] '
def __init__(self, conf, process_id, vpnservice, namespace):
self.DIALECT_MAP['v1'] = 'ikev1'
self.DIALECT_MAP['v2'] = 'ikev2'
super(StrongSwanProcess, self).__init__(conf, process_id,
vpnservice, namespace)
def _execute(self, cmd, check_exit_code=True, extra_ok_codes=None):
"""Execute 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)
return ip_wrapper.netns.execute(
[NS_WRAPPER,
'--mount_paths=/etc:%s/etc,/var/run:%s/var/run' % (
self.config_dir, self.config_dir),
'--cmd=%s' % ','.join(cmd)],
check_exit_code=check_exit_code,
extra_ok_codes=extra_ok_codes)
def copy_and_overwrite(self, from_path, to_path):
if os.path.exists(to_path):
shutil.rmtree(to_path)
shutil.copytree(from_path, to_path)
def ensure_configs(self):
"""Generate config files which are needed for StrongSwan.
If there is no directory, this function will create
dirs.
"""
self.ensure_config_dir(self.vpnservice)
self.ensure_config_file(
'ipsec.conf',
cfg.CONF.strongswan.ipsec_config_template,
self.vpnservice)
self.ensure_config_file(
'strongswan.conf',
cfg.CONF.strongswan.strongswan_config_template,
self.vpnservice)
self.ensure_config_file(
'ipsec.secrets',
cfg.CONF.strongswan.ipsec_secret_template,
self.vpnservice)
self.copy_and_overwrite(cfg.CONF.strongswan.default_config_area,
self._get_config_filename('strongswan.d'))
def get_status(self):
return self._execute([self.binary, 'status'],
extra_ok_codes=[1, 3])
def restart(self):
"""Restart the process."""
self.reload()
def reload(self):
"""Reload the process.
Sends a USR1 signal to ipsec starter which in turn reloads the whole
configuration on the running IKE daemon charon based on the actual
ipsec.conf. Currently established connections are not affected by
configuration changes.
"""
self._execute([self.binary, 'reload'])
def start(self):
"""Start the process for only auto=route mode now.
Note: if there is no namespace yet,
just do nothing, and wait next event.
"""
if not self.namespace:
return
self._execute([self.binary, 'start'])
def stop(self):
self._execute([self.binary, 'stop'])
self.connection_status = {}
class StrongSwanDriver(ipsec.IPsecDriver):
def create_process(self, process_id, vpnservice, namespace):
return StrongSwanProcess(
self.conf,
process_id,
vpnservice,
namespace)

View File

@ -0,0 +1,22 @@
# Configuration for {{vpnservice.name}}
config setup
conn %default
ikelifetime=60m
keylife=20m
rekeymargin=3m
keyingtries=1
authby=psk
mobike=no
{% for ipsec_site_connection in vpnservice.ipsec_site_connections%}
conn {{ipsec_site_connection.id}}
keyexchange={{ipsec_site_connection.ikepolicy.ike_version}}
left={{vpnservice.external_ip}}
leftsubnet={{vpnservice.subnet.cidr}}
leftid={{vpnservice.external_ip}}
leftfirewall=yes
right={{ipsec_site_connection.peer_address}}
rightsubnet={{ipsec_site_connection['peer_cidrs']|join(',')}}
rightid={{ipsec_site_connection.peer_id}}
auto=route
{% endfor %}

View File

@ -0,0 +1,3 @@
# Configuration for {{vpnservice.name}}{% for ipsec_site_connection in vpnservice.ipsec_site_connections %}
{{vpnservice.external_ip}} {{ipsec_site_connection.peer_id}} : PSK "{{ipsec_site_connection.psk}}"
{% endfor %}

View File

@ -0,0 +1,8 @@
charon {
load_modular = yes
plugins {
include strongswan.d/charon/*.conf
}
}
include strongswan.d/*.conf

View File

@ -26,6 +26,7 @@ from oslo_config import cfg
from neutron_vpnaas.extensions import vpnaas
from neutron_vpnaas.services.vpn.device_drivers import ipsec as ipsec_driver
from neutron_vpnaas.services.vpn.device_drivers import strongswan_ipsec
from neutron_vpnaas.tests import base
_uuid = uuidutils.generate_uuid
@ -71,15 +72,16 @@ FAKE_VPN_SERVICE = {
class BaseIPsecDeviceDriver(base.BaseTestCase):
def setUp(self, driver=ipsec_driver.OpenSwanDriver):
def setUp(self, driver=ipsec_driver.OpenSwanDriver,
ipsec_process='ipsec.OpenSwanProcess'):
super(BaseIPsecDeviceDriver, self).setUp()
for klass in [
'os.makedirs',
'os.path.isdir',
'neutron.agent.linux.utils.replace_file',
'neutron.common.rpc.create_connection',
'neutron_vpnaas.services.vpn.device_drivers.ipsec.'
'OpenSwanProcess._gen_config_content',
'neutron_vpnaas.services.vpn.device_drivers.'
'%s._gen_config_content' % ipsec_process,
'shutil.rmtree',
]:
mock.patch(klass).start()
@ -101,8 +103,9 @@ class BaseIPsecDeviceDriver(base.BaseTestCase):
class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
def setUp(self, driver=ipsec_driver.OpenSwanDriver):
super(IPSecDeviceLegacy, self).setUp(driver)
def setUp(self, driver=ipsec_driver.OpenSwanDriver,
ipsec_process='ipsec.OpenSwanProcess'):
super(IPSecDeviceLegacy, self).setUp(driver, ipsec_process)
self._make_router_info_for_test(iptables=self.iptables)
def _make_router_info_for_test(self, iptables=None):
@ -448,8 +451,9 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
class IPSecDeviceDVR(object):
def setUp(self, driver=ipsec_driver.OpenSwanDriver):
super(IPSecDeviceDVR, self).setUp(driver)
def setUp(self, driver=ipsec_driver.OpenSwanDriver,
ipsec_process='ipsec.OpenSwanProcess'):
super(IPSecDeviceDVR, self).setUp(driver, ipsec_process)
self._make_dvr_router_info_for_test(iptables=self.iptables)
def _make_dvr_router_info_for_test(self, iptables=None):
@ -554,3 +558,17 @@ class TestOpenSwanProcess(base.BaseTestCase):
def test__get_nexthop_fqdn_peer_addr_is_not_resolved(self):
self.assertRaises(vpnaas.VPNPeerAddressNotResolved,
self.driver._get_nexthop, 'foo.peer.addr')
class IPsecStrongswanDeviceDriverLegacy(IPSecDeviceLegacy):
def setUp(self, driver=strongswan_ipsec.StrongSwanDriver,
ipsec_process='strongswan_ipsec.StrongSwanProcess'):
super(IPsecStrongswanDeviceDriverLegacy, self).setUp(driver,
ipsec_process)
class IPsecStrongswanDeviceDriverDVR(IPSecDeviceDVR):
def setUp(self, driver=strongswan_ipsec.StrongSwanDriver,
ipsec_process='strongswan_ipsec.StrongSwanProcess'):
super(IPsecStrongswanDeviceDriverDVR, self).setUp(driver,
ipsec_process)