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:
parent
810bcf53e7
commit
6d55e377ea
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
163
neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py
Normal file
163
neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py
Normal 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)
|
@ -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 %}
|
@ -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 %}
|
@ -0,0 +1,8 @@
|
||||
charon {
|
||||
load_modular = yes
|
||||
plugins {
|
||||
include strongswan.d/charon/*.conf
|
||||
}
|
||||
}
|
||||
|
||||
include strongswan.d/*.conf
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user