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
This commit is contained in:
parent
bf0fb3b0e2
commit
4dde1f78e7
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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
|
||||
|
96
test/unit/drivers/test_strongswan.py
Normal file
96
test/unit/drivers/test_strongswan.py
Normal 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'),
|
||||
])
|
@ -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)
|
||||
|
@ -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'))
|
||||
|
Loading…
x
Reference in New Issue
Block a user