diff --git a/neutron_vpnaas/extensions/vpnaas.py b/neutron_vpnaas/extensions/vpnaas.py index 8adfe7de9..21bb861e7 100644 --- a/neutron_vpnaas/extensions/vpnaas.py +++ b/neutron_vpnaas/extensions/vpnaas.py @@ -87,6 +87,10 @@ class RouterIsNotExternal(nexception.BadRequest): message = _("Router %(router_id)s has no external network gateway set") +class VPNPeerAddressNotResolved(nexception.InvalidInput): + message = _("Peer address %(peer_address)s cannot be resolved") + + vpn_supported_initiators = ['bi-directional', 'response-only'] vpn_supported_encryption_algorithms = ['3des', 'aes-128', 'aes-192', 'aes-256'] diff --git a/neutron_vpnaas/services/vpn/device_drivers/ipsec.py b/neutron_vpnaas/services/vpn/device_drivers/ipsec.py index d150e5c2d..d3416e0a8 100644 --- a/neutron_vpnaas/services/vpn/device_drivers/ipsec.py +++ b/neutron_vpnaas/services/vpn/device_drivers/ipsec.py @@ -20,10 +20,12 @@ import os import re import shutil import six +import socket from neutron.agent.linux import ip_lib from neutron.agent.linux import utils +from neutron.api.v2 import attributes from neutron.common import rpc as n_rpc from neutron import context from neutron.i18n import _LE @@ -35,6 +37,7 @@ from oslo_concurrency import lockutils from oslo_config import cfg import oslo_messaging +from neutron_vpnaas.extensions import vpnaas from neutron_vpnaas.services.vpn.common import topics from neutron_vpnaas.services.vpn import device_drivers @@ -338,9 +341,25 @@ class OpenSwanProcess(BaseSwanProcess): self.start() return + def _resolve_fqdn(self, fqdn): + # The first addrinfo member from the list returned by + # socket.getaddrinfo is used for the address resolution. + # The code doesn't filter for ipv4 or ipv6 address. + try: + addrinfo = socket.getaddrinfo(fqdn, None)[0] + return addrinfo[-1][0] + except socket.gaierror: + LOG.exception(_LE("Peer address %s cannot be resolved"), fqdn) + raise vpnaas.VPNPeerAddressNotResolved(peer_address=fqdn) + def _get_nexthop(self, address): - routes = self._execute( - ['ip', 'route', 'get', address]) + # check if address is an ip address or fqdn + invalid_ip_address = attributes._validate_ip_address(address) + if invalid_ip_address: + ip_addr = self._resolve_fqdn(address) + else: + ip_addr = address + routes = self._execute(['ip', 'route', 'get', ip_addr]) if routes.find('via') >= 0: return routes.split(' ')[2] return address diff --git a/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py b/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py index 9784d72c9..2a1537ab2 100644 --- a/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py +++ b/neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py @@ -15,10 +15,12 @@ import contextlib import copy import mock +import socket from neutron.openstack.common import uuidutils from neutron.plugins.common import constants +from neutron_vpnaas.extensions import vpnaas from neutron_vpnaas.services.vpn.device_drivers import ipsec as ipsec_driver from neutron_vpnaas.tests import base @@ -195,7 +197,7 @@ class TestIPsecDeviceDriver(base.BaseTestCase): self.assertEqual(ensure_p.call_count, 0) def test__sync_vpn_processes_router_with_no_vpn_and_no_vpn_services(self): - """No vpn services running and router not hosting vpn svc""" + """No vpn services running and router not hosting vpn svc.""" router_id_no_vpn = _uuid() self.driver.process_status_cache = {} self.driver.processes = {} @@ -384,3 +386,71 @@ class TestIPsecDeviceDriver(base.BaseTestCase): missing_conn = new_status['ipsec_site_connections'].get('20') self.assertIsNotNone(missing_conn) self.assertEqual(constants.DOWN, missing_conn['status']) + + +class TestOpenSwanProcess(base.BaseTestCase): + def setUp(self): + super(TestOpenSwanProcess, self).setUp() + self.driver = ipsec_driver.OpenSwanProcess(mock.ANY, 'foo-process-id', + FAKE_VPN_SERVICE, mock.ANY) + + def test__resolve_fqdn(self): + with mock.patch.object(socket, 'getaddrinfo') as mock_getaddr_info: + mock_getaddr_info.return_value = [(2, 1, 6, '', + ('172.168.1.2', 0))] + resolved_ip_addr = self.driver._resolve_fqdn('fqdn.foo.addr') + self.assertEqual('172.168.1.2', resolved_ip_addr) + + def _test_get_nexthop_helper(self, address, _resolve_fqdn_side_effect, + _execute_ret_val, expected_ip_cmd, + expected_nexthop): + with contextlib.nested( + mock.patch.object(self.driver, '_execute'), + mock.patch.object(self.driver, '_resolve_fqdn') + ) as (fake_execute, fake_resolve_fqdn): + fake_resolve_fqdn.side_effect = _resolve_fqdn_side_effect + fake_execute.return_value = _execute_ret_val + + returned_next_hop = self.driver._get_nexthop(address) + _resolve_fqdn_expected_call_count = ( + 1 if _resolve_fqdn_side_effect else 0) + + self.assertEqual(_resolve_fqdn_expected_call_count, + fake_resolve_fqdn.call_count) + fake_execute.assert_called_once_with(expected_ip_cmd) + self.assertEqual(expected_nexthop, returned_next_hop) + + def test__get_nexthop_peer_addr_is_ipaddr(self): + gw_addr = '10.0.0.1' + _fake_execute_ret_val = '172.168.1.2 via %s' % gw_addr + peer_address = '172.168.1.2' + expected_ip_cmd = ['ip', 'route', 'get', peer_address] + self._test_get_nexthop_helper(peer_address, None, + _fake_execute_ret_val, expected_ip_cmd, + gw_addr) + + def test__get_nexthop_peer_addr_is_valid_fqdn(self): + peer_address = 'foo.peer.addr' + expected_ip_cmd = ['ip', 'route', 'get', '172.168.1.2'] + gw_addr = '10.0.0.1' + _fake_execute_ret_val = '172.168.1.2 via %s' % gw_addr + + def _fake_resolve_fqdn(address): + return '172.168.1.2' + + self._test_get_nexthop_helper(peer_address, _fake_resolve_fqdn, + _fake_execute_ret_val, expected_ip_cmd, + gw_addr) + + def test__get_nexthop_gw_not_present(self): + peer_address = '172.168.1.2' + expected_ip_cmd = ['ip', 'route', 'get', '172.168.1.2'] + _fake_execute_ret_val = ' ' + + self._test_get_nexthop_helper(peer_address, None, + _fake_execute_ret_val, expected_ip_cmd, + peer_address) + + def test__get_nexthop_fqdn_peer_addr_is_not_resolved(self): + self.assertRaises(vpnaas.VPNPeerAddressNotResolved, + self.driver._get_nexthop, 'foo.peer.addr')