Add support for StrongSwan VPN to router

This change adds Strongswan to support VPNaaS in appliance.

Change-Id: I1adb74c159eaf4f62950d17ed015856e90a91641
Partial-Blueprint: neutron-vpnaas
This commit is contained in:
Mark McClain 2016-03-14 19:08:00 -04:00 committed by Adam Gandelman
parent 8633d1a5bc
commit 920954e31d
16 changed files with 413 additions and 9 deletions

View File

@ -6,6 +6,8 @@
bird_enable: False
bird6_enable: True
bird_enable_service: True
strongswan_enable: True
strongswan_enable_service: False
dnsmasq_conf_dir: /etc/dnsmasq.d
dnsmasq_conf_file: /etc/dnsmasq.conf
install_extras: False
@ -22,6 +24,7 @@
- include: tasks/astara.yml
- include: tasks/bird.yml
- include: tasks/conntrackd.yml
- include: tasks/strongswan.yml
- include: tasks/dnsmasq.yml
- include: tasks/extras.yml
when: install_extras

View File

@ -29,8 +29,9 @@
- name: install gunicorn config file
template: src=gunicorn.j2 dest=/etc/astara_gunicorn_config.py
- name: add gunicorn user
command: useradd -r gunicorn
action: user name=gunicorn state=present
- name: install init.d files
copy: src={{playbook_dir}}/../scripts/etc/init.d/{{item}} dest=/etc/init.d/{{item}} mode=0555
@ -90,4 +91,7 @@
command: apt-get -y autoremove
when: do_cleanup
- name: Ensure gunicorn is restarted
service: name=astara-router-api-server state=restarted enabled=yes

View File

@ -0,0 +1,9 @@
---
- name: install strongswan
apt: name=strongswan state=installed install_recommends=yes
when: strongswan_enable
- name: Ensure strongswan is started
service: name=strongswan state=started enabled=yes
when: strongswan_enable and strongswan_enable_service

View File

View File

@ -0,0 +1,89 @@
# Copyright 2016 Akanda, Inc
#
# 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 jinja2
from astara_router.drivers import base
from astara_router import utils
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'templates')
class StrongswanManager(base.Manager):
"""
A class to interact with strongswan, an IPSEC VPN daemon.
"""
def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'):
"""
Initializes StrongswanManager class.
:type root_helper: string
:param root_helper: System utility used to gain escalate privileges.
"""
super(StrongswanManager, self).__init__(root_helper)
def save_config(self, config):
"""
Writes config file for strongswan daemon.
:type config: astara_router.models.Configuration
:param config:
{'ge0': 'eth0', 'ge1': 'eth1'}
"""
templates = ('ipsec.conf', 'ipsec.secrets')
for template_name in templates:
tmpl = jinja2.Template(
open(os.path.join(TEMPLATE_DIR, template_name+'.j2')).read()
)
tmp = os.path.join('/tmp', template_name)
open(tmp, 'w').write(tmpl.render(vpnservices=config.vpn))
for template_name in templates:
tmp = os.path.join('/tmp', template_name)
etc = os.path.join('/etc', template_name)
utils.execute(['mv', tmp, etc], self.root_helper)
def restart(self):
"""
Restart the Strongswan daemon using the system provided init scripts.
"""
try:
utils.execute(
['service', 'strongswan', 'status'],
self.root_helper
)
except: # pragma no cover
utils.execute(['service', 'strongswan', 'start'], self.root_helper)
else: # pragma no cover
utils.execute(
['service', 'strongswan', 'reload'],
self.root_helper
)
def stop(self):
"""
Stop the Strongswan daemon using the system provided init scripts.
"""
try:
utils.execute(
['service', 'strongswan', 'stop'],
self.root_helper
)
except: # pragma no cover
pass

View File

@ -0,0 +1,3 @@
These templates were originally copied from the upstream neutron-vpaas [1].
[1] http://git.openstack.org/cgit/openstack/neutron-vpnaas/tree/neutron_vpnaas/services/vpn/device_drivers/template/strongswan

View File

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

View File

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

View File

@ -22,6 +22,7 @@ from astara_router import models
from astara_router import settings
from astara_router.drivers import (bird, conntrackd, dnsmasq, ip, metadata,
iptables, arp, hostname, loadbalancer)
from astara_router.drivers.vpn import ipsec
class ServiceManagerBase(object):
@ -114,6 +115,7 @@ class RouterManager(ServiceManagerBase):
self.update_routes(cache)
self.update_arp()
self.update_conntrackd()
self.update_ipsec_vpn()
self.reload_config()
def update_conntrackd(self):
@ -162,6 +164,15 @@ class RouterManager(ServiceManagerBase):
)
mgr.remove_stale_entries(self._config)
def update_ipsec_vpn(self):
mgr = ipsec.StrongswanManager()
if self._config.vpn:
mgr.save_config(self._config)
mgr.restart()
else:
mgr.stop()
def get_interfaces(self):
return self.ip_mgr.get_interfaces()

View File

@ -16,6 +16,7 @@
import abc
import itertools
import re
import netaddr
@ -313,8 +314,9 @@ class Label(ModelBase):
class Subnet(ModelBase):
def __init__(self, cidr, gateway_ip, dhcp_enabled=True,
def __init__(self, id_, cidr, gateway_ip, dhcp_enabled=True,
dns_nameservers=None, host_routes=None):
self.id = id_
self.cidr = cidr
self.gateway_ip = gateway_ip
self.dhcp_enabled = bool(dhcp_enabled)
@ -353,6 +355,7 @@ class Subnet(ModelBase):
host_routes = [StaticRoute(r['destination'], r['nexthop'])
for r in d.get('host_routes', [])]
return cls(
d['id'],
d['cidr'],
d['gateway_ip'],
d['dhcp_enabled'],
@ -663,6 +666,211 @@ class FixedIp(ModelBase):
return dict((f, getattr(self, f)) for f in fields)
class DeadPeerDetection(ModelBase):
def __init__(self, action, interval, timeout):
self.action = action
self.interval = interval
self.timeout = timeout
@classmethod
def from_dict(cls, d):
return cls(
d['action'],
d['interval'],
d['timeout']
)
class Lifetime(ModelBase):
def __init__(self, units, value):
self.units = units
self.value = value
@classmethod
def from_dict(cls, d):
return cls(
d['units'],
d['value']
)
class EndpointGroup(ModelBase):
def __init__(self, id_, tenant_id, name, type_, endpoints=()):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.type = type_
if type_ == 'cidr':
self.endpoints = [netaddr.IPNetwork(ep) for ep in endpoints]
else:
self.endpoints = endpoints
self.subnet_map = {}
@property
def cidrs(self):
if self.type == 'subnet':
return [
self.subnet_map[ep].cidr
for ep in self.endpoints
if ep in self.subnet_map
]
else:
return self.endpoints
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['tenant_id'],
d['name'],
d['type'],
d['endpoints']
)
class IkePolicy(ModelBase):
def __init__(self, id_, tenant_id, name, ike_version, auth_algorithm,
encryption_algorithm, pfs, phase1_negotiation_mode, lifetime):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.ike_version = ike_version
self.auth_algorithm = auth_algorithm
self.encryption_algorithm = encryption_algorithm
self.pfs = pfs
self.phase1_negotiation_mode = phase1_negotiation_mode
self.lifetime = lifetime
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['tenant_id'],
d['name'],
d['ike_version'],
d['auth_algorithm'],
d['encryption_algorithm'],
d['pfs'],
d['phase1_negotiation_mode'],
Lifetime.from_dict(d['lifetime'])
)
class IpsecPolicy(ModelBase):
def __init__(self, id_, tenant_id, name, transform_protocol,
auth_algorithm, encryption_algorithm, encapsulation_mode,
lifetime, pfs):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.transform_protocol = transform_protocol
self.auth_algorithm = auth_algorithm
self.encryption_algorithm = encryption_algorithm
self.encapsulation_mode = encapsulation_mode
self.lifetime = lifetime
self.pfs = pfs
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['tenant_id'],
d['name'],
d['transform_protocol'],
d['auth_algorithm'],
d['encryption_algorithm'],
d['encapsulation_mode'],
Lifetime.from_dict(d['lifetime']),
d['pfs']
)
class IpsecSiteConnection(ModelBase):
def __init__(self, id_, tenant_id, name, peer_address, peer_id,
admin_state_up, route_mode, mtu, initiator, auth_mode, psk,
dpd, status, vpnservice_id, local_ep_group=None,
peer_ep_group=None, peer_cidrs=[], ikepolicy=None,
ipsecpolicy=None):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.peer_address = netaddr.IPAddress(peer_address)
self.peer_id = peer_id
self.route_mode = route_mode
self.mtu = mtu
self.initiator = initiator
self.auth_mode = auth_mode
self.psk = psk
self.dpd = dpd
self.status = status
self.admin_state_up = admin_state_up
self.vpnservice_id = vpnservice_id
self.ipsecpolicy = ipsecpolicy
self.ikepolicy = ikepolicy
self.local_ep_group = local_ep_group
self.peer_ep_group = peer_ep_group
self.peer_cidrs = [netaddr.IPNetwork(pc) for pc in peer_cidrs]
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['tenant_id'],
d['name'],
d['peer_address'],
d['peer_id'],
d['admin_state_up'],
d['route_mode'],
d['mtu'],
d['initiator'],
d['auth_mode'],
d['psk'],
DeadPeerDetection.from_dict(d['dpd']),
d['status'],
d['vpnservice_id'],
peer_cidrs=d['peer_cidrs'],
ikepolicy=IkePolicy.from_dict(d['ikepolicy']),
ipsecpolicy=IpsecPolicy.from_dict(d['ipsecpolicy']),
local_ep_group=EndpointGroup.from_dict(d['local_ep_group']),
peer_ep_group=EndpointGroup.from_dict(d['peer_ep_group']),
)
class VpnService(ModelBase):
def __init__(self, id_, name, status, admin_state_up, external_v4_ip,
external_v6_ip, router_id, subnet_id=None,
ipsec_site_connections=()):
self.id = id_
self.name = name
self.status = status
self.admin_state_up = admin_state_up
self.external_v4_ip = netaddr.IPAddress(external_v4_ip)
self.external_v6_ip = netaddr.IPAddress(external_v6_ip)
self.router_id = router_id
self.subnet_id = subnet_id
self.ipsec_site_connections = ipsec_site_connections
def get_external_ip(self, peer_ip):
if peer_ip.version == '6':
return self.external_v6_ip
else:
return self.external_v4_ip
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['name'],
d['status'],
d['admin_state_up'],
d['external_v4_ip'],
d['external_v6_ip'],
d['router_id'],
d.get('subnet_id'),
[IpsecSiteConnection.from_dict(c) for c in d['ipsec_connections']]
)
class SystemConfiguration(ModelBase):
service_name = 'system'
@ -740,6 +948,13 @@ class RouterConfiguration(SystemConfiguration):
self._attach_floating_ips(self.floating_ips)
self.vpn = [
VpnService.from_dict(s)
for s in conf_dict.get('vpn', {}).get('ipsec', [])
]
self._link_subnets()
def validate(self):
"""Validate anchor rules to ensure that ifaces and tables exist."""
interfaces = set(n.interface.ifname for n in self.networks)
@ -788,8 +1003,22 @@ class RouterConfiguration(SystemConfiguration):
if fip.fixed_ip in int_cidr:
fip.network = net
def _link_subnets(self):
subnet_map = {}
for n in self.networks:
for s in n.subnets:
subnet_map[s.id] = s
vpn_conn_generator = (v.ipsec_site_connections for v in self.vpn)
for conn in itertools.chain.from_iterable(vpn_conn_generator):
if conn.local_ep_group.type == 'subnet':
conn.local_ep_group.subnet_map = subnet_map
def to_dict(self):
fields = ('networks', 'address_book', 'anchors', 'static_routes')
fields = (
'networks', 'address_book', 'anchors', 'static_routes', 'vpn'
)
return dict((f, getattr(self, f)) for f in fields)
@property

View File

@ -41,5 +41,8 @@ ip6tables: CommandFilter, ip6tables, root
# astara_router/drivers/metadata.py:
mv_metadata: RegExpFilter, mv, root, mv, /tmp/metadata\.conf, /etc/metadata\.conf
# astara_router/drivers/vpn/ipsec.py:
mv_strongswan: RegExpFilter, mv, root, mv, /tmp/ipsec.*, /etc/ipsec.*
# astara services
services: CommandFilter, service, root

View File

@ -114,7 +114,8 @@ class SystemAPITestCase(unittest.TestCase):
'interfaces': [],
'management_address': None,
'tenant_id': None
}
},
'vpn': [],
}
}
self.assertEqual(json.loads(result.data), expected)

View File

@ -103,6 +103,7 @@ class ARPTest(unittest2.TestCase):
'name': 'ext',
},
'subnets': [{
'id': 'theid',
'cidr': '172.16.77.0/24',
'gateway_ip': '172.16.77.1',
'dhcp_enabled': True,

View File

@ -31,6 +31,7 @@ CONFIG = models.RouterConfiguration({
'name': 'ext',
'network_type': models.Network.TYPE_EXTERNAL,
'subnets': [{
'id': 'theid',
'cidr': '172.16.77.0/24',
'gateway_ip': '172.16.77.1',
'dhcp_enabled': True,
@ -48,6 +49,7 @@ CONFIG = models.RouterConfiguration({
'name': 'internal',
'network_type': models.Network.TYPE_INTERNAL,
'subnets': [{
'id': 'theid',
'cidr': '192.168.0.0/24',
'gateway_ip': '192.168.0.1',
'dhcp_enabled': True,

View File

@ -176,6 +176,7 @@ class RouteTest(unittest2.TestCase):
def test_update_default_v4_from_subnet(self):
subnet = dict(
id='theid',
cidr='192.168.89.0/24',
gateway_ip='192.168.89.1',
dhcp_enabled=True,
@ -198,12 +199,14 @@ class RouteTest(unittest2.TestCase):
def test_update_multiple_v4_subnets(self):
subnet = dict(
id='id-1',
cidr='192.168.89.0/24',
gateway_ip='192.168.89.1',
dhcp_enabled=True,
dns_nameservers=[],
)
subnet2 = dict(
id='id-2',
cidr='192.168.71.0/24',
gateway_ip='192.168.71.1',
dhcp_enabled=True,
@ -226,6 +229,7 @@ class RouteTest(unittest2.TestCase):
def test_update_default_v6(self):
subnet = dict(
id='theid',
cidr='fe80::1/64',
gateway_ip='fe80::1',
dhcp_enabled=True,
@ -248,12 +252,14 @@ class RouteTest(unittest2.TestCase):
def test_update_default_multiple_v6(self):
subnet = dict(
id='id-1',
cidr='fe80::1/64',
gateway_ip='fe80::1',
dhcp_enabled=True,
dns_nameservers=[],
)
subnet2 = dict(
id='id-2',
cidr='fe89::1/64',
gateway_ip='fe89::1',
dhcp_enabled=True,
@ -278,6 +284,7 @@ class RouteTest(unittest2.TestCase):
lambda *a, **kw: None)
def test_custom_host_routes(self):
subnet = dict(
id='theid',
cidr='192.168.89.0/24',
gateway_ip='192.168.89.1',
dhcp_enabled=True,
@ -367,6 +374,7 @@ class RouteTest(unittest2.TestCase):
self.assertEqual(len(cache.get('host_routes')), 1)
sudo.reset_mock()
network['subnets'].append(dict(
id='add-1',
cidr='192.168.90.0/24',
gateway_ip='192.168.90.1',
dhcp_enabled=True,
@ -402,6 +410,7 @@ class RouteTest(unittest2.TestCase):
def test_custom_host_routes_failure(self):
subnet = dict(
id='theid',
cidr='192.168.89.0/24',
gateway_ip='192.168.89.1',
dhcp_enabled=True,

View File

@ -289,9 +289,16 @@ class StaticRouteTestCase(TestCase):
class SubnetTestCase(TestCase):
def test_subnet(self):
s = models.Subnet('192.168.1.0/24', '192.168.1.1', True, ['8.8.8.8'],
[])
s = models.Subnet(
'id',
'192.168.1.0/24',
'192.168.1.1',
True,
['8.8.8.8'],
[]
)
self.assertEqual(s.id, 'id')
self.assertEqual(s.cidr, netaddr.IPNetwork('192.168.1.0/24'))
self.assertEqual(s.gateway_ip, netaddr.IPAddress('192.168.1.1'))
self.assertTrue(s.dhcp_enabled)
@ -299,12 +306,12 @@ class SubnetTestCase(TestCase):
self.assertEqual(s.host_routes, [])
def test_gateway_ip_empty(self):
s = models.Subnet('192.168.1.0/24', '', True, ['8.8.8.8'],
s = models.Subnet('id', '192.168.1.0/24', '', True, ['8.8.8.8'],
[])
self.assertIsNone(s.gateway_ip)
def test_gateway_ip_none(self):
s = models.Subnet('192.168.1.0/24', None, True, ['8.8.8.8'],
s = models.Subnet('id', '192.168.1.0/24', None, True, ['8.8.8.8'],
[])
self.assertIsNone(s.gateway_ip)
@ -365,6 +372,7 @@ class NetworkTestCase(TestCase):
class RouterConfigurationTestCase(TestCase):
def test_init_only_networks(self):
subnet = dict(
id='id',
cidr='192.168.1.0/24',
gateway_ip='192.168.1.1',
dhcp_enabled=True,
@ -542,7 +550,8 @@ class RouterConfigurationTestCase(TestCase):
expected = dict(networks=[],
address_book={},
static_routes=[],
anchors=[])
anchors=[],
vpn=[])
self.assertEqual(c.to_dict(), expected)