Ensure VPN settings are more prescriptive.

Previously VPN service relied on default behaviours and an open
firewall.  This specifies more values and ensures the firewall is
properly set.  Additionally, test coverage is expanded.

Closes-Bug:1564213
Change-Id: Iefaccddaad54c412195802f97811722bb593b2ca
(cherry picked from commit 4dde1f78e7)
This commit is contained in:
Mark McClain 2016-03-24 22:41:48 +00:00 committed by Adam Gandelman
parent cd26f8925c
commit c1a3c72515
8 changed files with 362 additions and 11 deletions

View File

@ -29,6 +29,9 @@ API_SERVICE = 5000
DHCP = 67
DHCPV6 = 546
ISAKMP = 500
IPSEC_NAT_T = 4500
NFS_DEVELOPMENT = [111, 1110, 2049, 4045]
MANAGEMENT_PORTS = [SSH, API_SERVICE] # + NFS_DEVELOPMENT

View File

@ -159,7 +159,9 @@ class IPTablesManager(base.Manager):
return itertools.chain(
self._build_default_filter_rules(),
self._build_management_filter_rules(config),
self._build_internal_network_filter_rules(config)
self._build_internal_network_filter_rules(config),
self._build_vpn_filter_rules(config),
[Rule('COMMIT')]
)
def _build_default_filter_rules(self):
@ -258,7 +260,33 @@ class IPTablesManager(base.Manager):
'--state RELATED,ESTABLISHED -j ACCEPT' % ext_if.ifname
))
rules.append(Rule('COMMIT'))
return rules
def _build_vpn_filter_rules(self, config):
rules = []
ext_net = self.get_external_network(config)
if ext_net:
ext_if = ext_net.interface
else:
ext_net = None
if ext_net is None or not config.vpn:
return rules
template = (
('-A INPUT -i %%s -p udp -m udp --dport %d -j ACCEPT ' %
settings.ISAKMP),
('-A INPUT -i %%s -p udp -m udp --dport %d -j ACCEPT ' %
settings.IPSEC_NAT_T),
'-A INPUT -i %s -p esp -j ACCEPT',
'-A INPUT -i %s -p ah -j ACCEPT'
)
for version in (4, 6):
rules.extend(
(Rule(t % ext_if.ifname, ip_version=version) for t in template)
)
return rules
def _build_nat_table(self, config):

View File

@ -22,6 +22,20 @@ from astara_router import utils
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'templates')
STRONGSWAN_TRANSLATIONS = {
"3des": "3des",
"aes-128": "aes128",
"aes-256": "aes256",
"aes-192": "aes192",
"group2": "modp1024",
"group5": "modp1536",
"group14": "modp2048",
"group15": "modp3072",
"bi-directional": "start",
"response-only": "add",
}
class StrongswanManager(base.Manager):
"""
A class to interact with strongswan, an IPSEC VPN daemon.
@ -40,19 +54,21 @@ class StrongswanManager(base.Manager):
Writes config file for strongswan daemon.
:type config: astara_router.models.Configuration
:param config:
{'ge0': 'eth0', 'ge1': 'eth1'}
"""
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(TEMPLATE_DIR)
)
env.filters['strongswan'] = lambda v: STRONGSWAN_TRANSLATIONS.get(v, v)
templates = ('ipsec.conf', 'ipsec.secrets')
for template_name in templates:
tmpl = jinja2.Template(
open(os.path.join(TEMPLATE_DIR, template_name+'.j2')).read()
)
tmpl = env.get_template(template_name+'.j2')
tmp = os.path.join('/tmp', template_name)
open(tmp, 'w').write(tmpl.render(vpnservices=config.vpn))
utils.replace_file(tmp, tmpl.render(vpnservices=config.vpn))
for template_name in templates:
tmp = os.path.join('/tmp', template_name)

View File

@ -5,12 +5,12 @@ conn %default
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}}
authby=psk
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(',')}}
@ -20,6 +20,20 @@ conn {{ipsec_site_connection.id}}
rightsubnet={{ipsec_site_connection.peer_ep_group.cidrs|join(',')}}
rightid={{ipsec_site_connection.peer_id}}
auto=route
dpdaction={{ipsec_site_connection.dpd.action}}
dpddelay={{ipsec_site_connection.dpd.interval}}
dpdtimeout={{ipsec_site_connection.dpd.timeout}}
# ike
ike={{ipsec_site_connection.ikepolicy.encryption_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.auth_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.pfs|strongswan}}
ikelifetime={{ipsec_site_connection.ikepolicy.lifetime.value}}s
# ipsec
{{ipsec_site_connection.ipsecpolicy.transform_protocol}}={{ipsec_site_connection.ikepolicy.encryption_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.auth_algorithm|strongswan}}-{{ipsec_site_connection.ikepolicy.pfs|strongswan}}
lifetime={{ipsec_site_connection.ipsecpolicy.lifetime.value}}s
type={{ipsec_site_connection.ipsecpolicy.encapsulation_mode}}
{% endfor %}
{% endfor %}

View File

@ -41,3 +41,8 @@ console_scripts =
all_files = 1
build-dir = doc/build
source-dir = doc/source
[nosetests]
verbosity = 2
detailed-errors = 1
cover-package = astara_router

View File

@ -0,0 +1,96 @@
# Copyright 2014 DreamHost, LLC
#
# Author: DreamHost, LLC
#
# 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.
from unittest2 import TestCase
import mock
import netaddr
import re
import textwrap
from astara_router.drivers.vpn import ipsec
class StrongswanTestCase(TestCase):
"""
"""
def setUp(self):
self.mock_execute = mock.patch('astara_router.utils.execute').start()
self.mock_replace_file = mock.patch(
'astara_router.utils.replace_file'
).start()
self.addCleanup(mock.patch.stopall)
self.mgr = ipsec.StrongswanManager()
def test_save_config(self):
mock_config = mock.Mock()
with mock.patch.object(ipsec, 'jinja2') as mock_jinja:
mock_env = mock_jinja.Environment.return_value
mock_get_template = mock_env.get_template
mock_render_rv = mock_get_template.return_value.render.return_value
self.mgr.save_config(mock_config)
mock_get_template.assert_has_calls([
mock.call('ipsec.conf.j2'),
mock.call().render(vpnservices=mock_config.vpn),
mock.call('ipsec.secrets.j2'),
mock.call().render(vpnservices=mock_config.vpn),
])
self.mock_replace_file.assert_has_calls([
mock.call('/tmp/ipsec.conf', mock_render_rv),
mock.call('/tmp/ipsec.secrets', mock_render_rv),
])
sudo = 'sudo astara-rootwrap /etc/rootwrap.conf'
self.mock_execute.assert_has_calls([
mock.call(['mv','/tmp/ipsec.conf', '/etc/ipsec.conf'], sudo),
mock.call(
['mv', '/tmp/ipsec.secrets', '/etc/ipsec.secrets'],
sudo
),
])
def test_restart(self):
self.mgr.restart()
self.mock_execute.assert_has_calls([
mock.call(['service', 'strongswan', 'status'],
'sudo astara-rootwrap /etc/rootwrap.conf'),
mock.call(['service', 'strongswan', 'reload'],
'sudo astara-rootwrap /etc/rootwrap.conf'),
])
def test_restart_failure(self):
with mock.patch('astara_router.utils.execute') as execute:
execute.side_effect = [Exception('status failed!'), None]
self.mgr.restart()
execute.assert_has_calls([
mock.call(['service', 'strongswan', 'status'],
'sudo astara-rootwrap /etc/rootwrap.conf'),
mock.call(['service', 'strongswan', 'start'],
'sudo astara-rootwrap /etc/rootwrap.conf'),
])
def test_stop(self):
self.mgr.stop()
self.mock_execute.assert_has_calls([
mock.call(['service', 'strongswan', 'stop'],
'sudo astara-rootwrap /etc/rootwrap.conf'),
])

View File

@ -16,13 +16,15 @@ FAKE_SYSTEM_DICT = {
"allocations": [],
"subnets": [
{
"id": "98a6270e-cf5f-4a60-9d7f-0d4524c00606",
"host_routes": [],
"cidr": "192.168.0.0/24",
"gateway_ip": "192.168.0.1",
"dns_nameservers": [],
"dhcp_enabled": True
"dhcp_enabled": True,
},
{
"id": "ext-subnet-id",
"host_routes": [],
"cidr": "fdd6:a1fa:cfa8:6af6::/64",
"gateway_ip": "fdd6:a1fa:cfa8:6af6::1",
@ -45,6 +47,7 @@ FAKE_SYSTEM_DICT = {
"allocations": [],
"subnets": [
{
"id": "mgt-subnet-id",
"host_routes": [],
"cidr": "fdca:3ba5:a17a:acda::/64",
"gateway_ip": "fdca:3ba5:a17a:acda::1",
@ -108,7 +111,6 @@ FAKE_POOL_DICT = {
'tenant_id': u'd22b149cee9b4eac8349c517eda00b89'
}
FAKE_MEMBER_DICT = {
'address': u'192.168.0.194',
'admin_state_up': True,
@ -119,6 +121,99 @@ FAKE_MEMBER_DICT = {
'weight': 1
}
FAKE_LIFETIME_DICT = {
'units': u'seconds',
'value': 3600,
}
FAKE_DEAD_PEER_DETECTION_DICT = {
'action': u'hold',
'interval': 30,
'timeout': 120
}
FAKE_IKEPOLICY_DICT = {
'auth_algorithm': u'sha1',
'encryption_algorithm': u'aes-128',
'id': u'2b7dddc7-721f-4b93-bff3-20a7ff765726',
'ike_version': u'v1',
'lifetime': FAKE_LIFETIME_DICT,
'name': u'ikepolicy1',
'pfs': u'group5',
'phase1_negotiation_mode': u'main',
'tenant_id': u'd01558034b144068a4884fa7d8c03cc8'
}
FAKE_IPSECPOLICY_DICT = {
'auth_algorithm': u'sha1',
'encapsulation_mode': u'tunnel',
'encryption_algorithm': u'aes-128',
'id': u'48f7ab18-f900-4ebe-9ef6-b1cc675f4e51',
'lifetime': FAKE_LIFETIME_DICT,
'name': u'ipsecpolicy1',
'pfs': u'group5',
'tenant_id': u'd01558034b144068a4884fa7d8c03cc8',
'transform_protocol': u'esp'
}
FAKE_LOCAL_ENDPOINT_DICT = {
'endpoints': [u'98a6270e-cf5f-4a60-9d7f-0d4524c00606'],
'id': u'3fbb0b1f-3fbe-4f97-9ec7-eba7f6009b94',
'name': u'local',
'tenant_id': u'd01558034b144068a4884fa7d8c03cc8',
'type': u'subnet'
}
FAKE_PEER_ENDPOINT_DICT = {
'endpoints': ['172.31.155.0/24'],
'id': u'dc15b31c-54a6-4b83-a4b0-7a6b136bbb5b',
'name': u'peer',
'tenant_id': u'd01558034b144068a4884fa7d8c03cc8',
'type': u'cidr'
}
FAKE_IPSEC_CONNECTION_DICT = {
'admin_state_up': True,
'auth_mode': u'psk',
'dpd': FAKE_DEAD_PEER_DETECTION_DICT,
'id': u'bfb6da63-7979-405d-9193-eda5601cf74b',
'ikepolicy': FAKE_IKEPOLICY_DICT,
'initiator': u'bi-directional',
'ipsecpolicy': FAKE_IPSECPOLICY_DICT,
'local_ep_group': FAKE_LOCAL_ENDPOINT_DICT,
'mtu': 1420,
'name': u'theconn',
'peer_address': '172.24.4.129',
'peer_cidrs': [],
'peer_ep_group': FAKE_PEER_ENDPOINT_DICT,
'peer_id': u'172.24.4.129',
'psk': u'secrete',
'route_mode': u'static',
'status': u'PENDING_CREATE',
'tenant_id': u'd01558034b144068a4884fa7d8c03cc8',
'vpnservice_id': u'1d5ff89a-d03f-4d57-b696-34ef5c53ae28'
}
FAKE_IPSEC_VPNSERVICE_DICT = {
'admin_state_up': True,
'external_v4_ip': '172.24.4.2',
'external_v6_ip': '2001:db8::1',
'id': u'1d5ff89a-d03f-4d57-b696-34ef5c53ae28',
'ipsec_connections': [FAKE_IPSEC_CONNECTION_DICT],
'name': u'thevpn',
'router_id': u'3d6d9ede-9b20-4610-9804-54ce1ef2bb43',
'status': u'PENDING_CREATE',
'subnet_id': None
}
FAKE_VPN_DICT = {
'vpn': {
'ipsec': [FAKE_IPSEC_VPNSERVICE_DICT]
}
}
FAKE_SYSTEM_WITH_VPN_DICT = dict(FAKE_SYSTEM_DICT, vpn=FAKE_VPN_DICT['vpn'])
def fake_loadbalancer_dict(listener=False, pool=False, members=False):
lb_dict = copy(FAKE_LOADBALANCER_DICT)

View File

@ -706,3 +706,97 @@ class LoadBalancerConfigurationTest(TestCase):
errors = lb_conf.validate()
# id is required
self.assertEqual(len(errors), 1)
class VPNModelsTest(TestCase):
def _test_model(self, model, config_dict, skip_keys=()):
instance = model.from_dict(config_dict)
for k in config_dict.keys():
if k in skip_keys:
continue
self.assertEqual(getattr(instance, k), config_dict[k])
return instance
def test_lifetime_model(self):
self._test_model(models.Lifetime, fakes.FAKE_LIFETIME_DICT)
def test_dead_peer_model(self):
self._test_model(
models.DeadPeerDetection,
fakes.FAKE_DEAD_PEER_DETECTION_DICT
)
def test_endpoint_model_local(self):
self._test_model(models.EndpointGroup, fakes.FAKE_LOCAL_ENDPOINT_DICT)
def test_endpoint_model_peer(self):
conf_dict = fakes.FAKE_PEER_ENDPOINT_DICT
ep = self._test_model(models.EndpointGroup, conf_dict, ['endpoints'])
self.assertEqual(
ep.endpoints,
[netaddr.IPNetwork(conf_dict['endpoints'][0])]
)
def _test_policy_model(self, config_dict, model):
with mock.patch.object(models, 'Lifetime') as mock_life:
self._test_model(model, config_dict, ['lifetime'])
mock_life.from_dict.assert_called_once_with(config_dict['lifetime'])
def test_ikepolicy_model(self):
return self._test_policy_model(
fakes.FAKE_IKEPOLICY_DICT,
models.IkePolicy
)
def test_ipsecpolicy_model(self):
return self._test_policy_model(
fakes.FAKE_IPSECPOLICY_DICT,
models.IpsecPolicy
)
def test_ipsec_site_connection_model(self):
config_dict = fakes.FAKE_IPSEC_CONNECTION_DICT
skip_keys = [
'dpd',
'ikepolicy',
'ipsecpolicy',
'local_ep_group',
'peer_ep_group',
'peer_address'
]
conn = self._test_model(
models.IpsecSiteConnection,
config_dict,
skip_keys
)
self.assertEqual(
conn.peer_address,
netaddr.IPAddress(config_dict['peer_address'])
)
self.assertIsInstance(conn.local_ep_group, models.EndpointGroup)
self.assertEqual(conn.local_ep_group.name, 'local')
self.assertIsInstance(conn.peer_ep_group, models.EndpointGroup)
self.assertEqual(conn.peer_ep_group.name, 'peer')
self.assertIsInstance(conn.dpd, models.DeadPeerDetection)
self.assertIsInstance(conn.ikepolicy, models.IkePolicy)
self.assertIsInstance(conn.ipsecpolicy, models.IpsecPolicy)
def test_vpnservice_model(self):
config_dict = fakes.FAKE_IPSEC_VPNSERVICE_DICT
vpn = self._test_model(
models.VpnService,
config_dict,
['ipsec_connections', 'external_v4_ip', 'external_v6_ip']
)
self.assertEqual(vpn.external_v4_ip, netaddr.IPAddress('172.24.4.2'))
self.assertEqual(vpn.external_v6_ip, netaddr.IPAddress('2001:db8::1'))