From 540d4cec6a533aa84ab70faa31dc3bb5fe86e30b Mon Sep 17 00:00:00 2001 From: Hunt Xu Date: Thu, 26 Oct 2017 18:55:11 +0800 Subject: [PATCH] VPNaaS: support subnet to subnet NAT VPNs often connect networks in the RFC-1918 address space. If both ends use the same address space, there will be IP conflicts. This commit enables using NAT to resolve such IP conflicts. https://libreswan.org/wiki/Subnet_to_subnet_using_NAT Fixes: redmine #11181 Signed-off-by: Hunt Xu --- ...e812868e6f_add_nat_support_to_ipsec_vpn.py | 41 +++++++++++++++++++ .../alembic_migrations/versions/HEAD | 2 +- neutron/db/vpn/vpn_db.py | 18 ++++---- neutron/db/vpn/vpn_validator.py | 16 +++++++- neutron/extensions/vpnaas.py | 8 ++++ neutron/services/vpn/agent.py | 12 ++++-- neutron/services/vpn/device_drivers/ipsec.py | 40 ++++++++++++++---- .../template/openswan/ipsec.conf.template | 4 ++ 8 files changed, 118 insertions(+), 23 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/44e812868e6f_add_nat_support_to_ipsec_vpn.py diff --git a/neutron/db/migration/alembic_migrations/versions/44e812868e6f_add_nat_support_to_ipsec_vpn.py b/neutron/db/migration/alembic_migrations/versions/44e812868e6f_add_nat_support_to_ipsec_vpn.py new file mode 100644 index 00000000000..3f0819e5822 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/44e812868e6f_add_nat_support_to_ipsec_vpn.py @@ -0,0 +1,41 @@ +# Copyright 2017 OpenStack Foundation +# +# 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. +# + +"""add NAT support to IPSec VPN + +Revision ID: 44e812868e6f +Revises: 1ec373736f8b +Create Date: 2017-10-31 16:06:24.427885 + +""" + +# revision identifiers, used by Alembic. +revision = '44e812868e6f' +down_revision = '1ec373736f8b' + +from alembic import op +import sqlalchemy as sa + + + +def upgrade(): + op.add_column( + 'ipsec_site_connections', + sa.Column('local_cidr', sa.String(length=32), nullable=True) + ) + + +def downgrade(): + op.drop_column('ipsec_site_connections', 'local_cidr') diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 3c0ec3d13c1..2ffcd1dc8fc 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -1ec373736f8b +44e812868e6f diff --git a/neutron/db/vpn/vpn_db.py b/neutron/db/vpn/vpn_db.py index 7cd7b6b9f4f..6908ead95d6 100644 --- a/neutron/db/vpn/vpn_db.py +++ b/neutron/db/vpn/vpn_db.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import netaddr import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.orm import exc @@ -110,6 +109,7 @@ class IPsecSiteConnection(model_base.BASEV2, description = sa.Column(sa.String(255)) peer_address = sa.Column(sa.String(255), nullable=False) peer_id = sa.Column(sa.String(255), nullable=False) + local_cidr = sa.Column(sa.String(32)) route_mode = sa.Column(sa.String(8), nullable=False) mtu = sa.Column(sa.Integer, nullable=False) initiator = sa.Column(sa.Enum("bi-directional", "response-only", @@ -248,6 +248,7 @@ def _make_ipsec_site_connection_dict(self, ipsec_site_conn, fields=None): 'description': ipsec_site_conn['description'], 'peer_address': ipsec_site_conn['peer_address'], 'peer_id': ipsec_site_conn['peer_id'], + 'local_cidr': ipsec_site_conn['local_cidr'], 'route_mode': ipsec_site_conn['route_mode'], 'mtu': ipsec_site_conn['mtu'], 'auth_mode': ipsec_site_conn['auth_mode'], @@ -269,11 +270,9 @@ def _make_ipsec_site_connection_dict(self, ipsec_site_conn, fields=None): return self._fields(res, fields) - def _get_subnet_ip_version(self, context, vpnservice_id): + def _get_subnet_cidr(self, context, vpnservice_id): vpn_service_db = self._get_vpnservice(context, vpnservice_id) - subnet = vpn_service_db.subnet['cidr'] - ip_version = netaddr.IPNetwork(subnet).version - return ip_version + return vpn_service_db.subnet['cidr'] def create_ipsec_site_connection(self, context, ipsec_site_connection, validator=None): @@ -293,10 +292,10 @@ def create_ipsec_site_connection(self, context, ipsec_site_connection, IPsecPolicy, ipsec_sitecon['ipsecpolicy_id']) vpnservice_id = ipsec_sitecon['vpnservice_id'] - ip_version = self._get_subnet_ip_version(context, vpnservice_id) + subnet_cidr = self._get_subnet_cidr(context, vpnservice_id) validator.validate_ipsec_site_connection(context, ipsec_sitecon, - ip_version) + subnet_cidr) ipsec_site_conn_db = IPsecSiteConnection( id=uuidutils.generate_uuid(), tenant_id=tenant_id, @@ -304,6 +303,7 @@ def create_ipsec_site_connection(self, context, ipsec_site_connection, description=ipsec_sitecon['description'], peer_address=ipsec_sitecon['peer_address'], peer_id=ipsec_sitecon['peer_id'], + local_cidr=ipsec_sitecon['local_cidr'], route_mode='static', mtu=ipsec_sitecon['mtu'], auth_mode='psk', @@ -339,13 +339,13 @@ def update_ipsec_site_connection( IPsecSiteConnection, ipsec_site_conn_id) vpnservice_id = ipsec_site_conn_db['vpnservice_id'] - ip_version = self._get_subnet_ip_version(context, vpnservice_id) + subnet_cidr = self._get_subnet_cidr(context, vpnservice_id) validator.assign_sensible_ipsec_sitecon_defaults( ipsec_sitecon, ipsec_site_conn_db) validator.validate_ipsec_site_connection( context, ipsec_sitecon, - ip_version) + subnet_cidr) self.assert_update_allowed(ipsec_site_conn_db) if "peer_cidrs" in ipsec_sitecon: diff --git a/neutron/db/vpn/vpn_validator.py b/neutron/db/vpn/vpn_validator.py index 4316bc29190..25fb95b28f9 100644 --- a/neutron/db/vpn/vpn_validator.py +++ b/neutron/db/vpn/vpn_validator.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import netaddr + from neutron.db import l3_db from neutron.extensions import vpnaas from neutron import manager @@ -52,6 +54,14 @@ def _check_mtu(self, context, mtu, ip_version): raise vpnaas.IPsecSiteConnectionMtuError(mtu=mtu, version=ip_version) + def _check_local_cidr(self, ipsec_sitecon, subnet): + local_cidr = ipsec_sitecon.get('local_cidr') + if local_cidr: + if netaddr.IPNetwork(local_cidr).prefixlen != subnet.prefixlen: + raise vpnaas.IPsecSiteConnectionLocalCIDRError( + local_cidr=local_cidr, + subnet_cidr='%s' % subnet.cidr) + def assign_sensible_ipsec_sitecon_defaults(self, ipsec_sitecon, prev_conn=None): """Provide defaults for optional items, if missing. @@ -73,12 +83,14 @@ def assign_sensible_ipsec_sitecon_defaults(self, ipsec_sitecon, prev_conn['dpd_timeout']) def validate_ipsec_site_connection(self, context, ipsec_sitecon, - ip_version): + subnet_cidr): """Reference implementation of validation for IPSec connection.""" + subnet = netaddr.IPNetwork(subnet_cidr) self._check_dpd(ipsec_sitecon) + self._check_local_cidr(ipsec_sitecon, subnet) mtu = ipsec_sitecon.get('mtu') if mtu: - self._check_mtu(context, mtu, ip_version) + self._check_mtu(context, mtu, subnet.version) def _check_router(self, context, router_id): router = self.l3_plugin.get_router(context, router_id) diff --git a/neutron/extensions/vpnaas.py b/neutron/extensions/vpnaas.py index e8bb5e8d7bb..8911dfeb64d 100644 --- a/neutron/extensions/vpnaas.py +++ b/neutron/extensions/vpnaas.py @@ -43,6 +43,11 @@ class IPsecSiteConnectionMtuError(qexception.InvalidInput): "for ipv%(version)s") +class IPsecSiteConnectionLocalCIDRError(qexception.InvalidInput): + message = _("ipsec_site_connection prefixlens of local CIDR %(local_cidr)s " + "and subnet CIDR %(subnet_cidr)s are not equal") + + class IKEPolicyNotFound(qexception.NotFound): message = _("IKEPolicy %(ikepolicy_id)s could not be found") @@ -179,6 +184,9 @@ class PPTPUsernameAlreadyExists(qexception.BadRequest): 'convert_to': attr.convert_to_list, 'validate': {'type:subnet_list': None}, 'is_visible': True}, + 'local_cidr': {'allow_post': True, 'allow_put': True, + 'validate': {'type:subnet_or_none': None}, + 'is_visible': True, 'default': None}, 'route_mode': {'allow_post': False, 'allow_put': False, 'default': 'static', 'is_visible': True}, diff --git a/neutron/services/vpn/agent.py b/neutron/services/vpn/agent.py index 284e02aacd6..6c202a25160 100644 --- a/neutron/services/vpn/agent.py +++ b/neutron/services/vpn/agent.py @@ -67,7 +67,7 @@ def get_namespace(self, router_id): return return router_info.ns_name - def add_nat_rule(self, router_id, chain, rule, top=False): + def add_nat_rule(self, router_id, chain, rule, top=False, tag=None): """Add nat rule in namespace. :param router_id: router_id @@ -81,9 +81,9 @@ def add_nat_rule(self, router_id, chain, rule, top=False): if not router_info: return router_info.iptables_manager.ipv4['nat'].add_rule( - chain, rule, top=top) + chain, rule, top=top, tag=tag) - def remove_nat_rule(self, router_id, chain, rule, top=False): + def remove_nat_rule(self, router_id, chain, rule, top=False, _tag=None): """Remove nat rule in namespace. :param router_id: router_id @@ -98,6 +98,12 @@ def remove_nat_rule(self, router_id, chain, rule, top=False): router_info.iptables_manager.ipv4['nat'].remove_rule( chain, rule, top=top) + def remove_nat_rules_by_tag(self, router_id, tag): + router_info = self.router_info.get(router_id) + if not router_info: + return + router_info.iptables_manager.ipv4['nat'].clear_rules_by_tag(tag) + def iptables_apply(self, router_id): """Apply IPtables. diff --git a/neutron/services/vpn/device_drivers/ipsec.py b/neutron/services/vpn/device_drivers/ipsec.py index ea5e2b93f2d..f8bc9e79449 100644 --- a/neutron/services/vpn/device_drivers/ipsec.py +++ b/neutron/services/vpn/device_drivers/ipsec.py @@ -75,6 +75,9 @@ } IPSEC_CONNS = 'ipsec_site_connections' +NETMAP_DNAT_RULE = ('-i qg-+ -s %s -d %s -m policy --dir in --pol ipsec ' + '-j NETMAP --to %s') +NETMAP_SNAT_RULE = '-o qg-+ -s %s -d %s -j NETMAP --to %s' def _get_template(template_file): @@ -522,17 +525,38 @@ def _update_nat(self, vpnservice, func): :param vpnservice: vpnservices :param func: self.add_nat_rule or self.remove_nat_rule """ - local_cidr = vpnservice['subnet']['cidr'] + local_subnet_cidr = vpnservice['subnet']['cidr'] router_id = vpnservice['router_id'] + rule_tag = vpnservice['id'] + self.agent.remove_nat_rules_by_tag(router_id, rule_tag) for ipsec_site_connection in vpnservice['ipsec_site_connections']: + local_cidr = ipsec_site_connection['local_cidr'] for peer_cidr in ipsec_site_connection['peer_cidrs']: - func( - router_id, - 'POSTROUTING', - '-s %s -d %s -m policy ' - '--dir out --pol ipsec ' - '-j ACCEPT ' % (local_cidr, peer_cidr), - top=True) + if local_cidr: + func( + router_id, + 'POSTROUTING', + '-s %s -d %s -m policy ' + '--dir out --pol ipsec ' + '-j ACCEPT ' % (local_cidr, peer_cidr), + top=True, tag=rule_tag) + func(router_id, 'PREROUTING', + NETMAP_DNAT_RULE % ( + peer_cidr, local_cidr, local_subnet_cidr), + top=True, tag=rule_tag) + func(router_id, 'POSTROUTING', + NETMAP_SNAT_RULE % ( + local_subnet_cidr, peer_cidr, local_cidr), + top=True, tag=rule_tag) + else: + func( + router_id, + 'POSTROUTING', + '-s %s -d %s -m policy ' + '--dir out --pol ipsec ' + '-j ACCEPT ' % (local_subnet_cidr, peer_cidr), + top=True) + self.agent.iptables_apply(router_id) def vpnservice_updated(self, context, **kwargs): diff --git a/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template b/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template index d39bcf9799b..1a045413186 100644 --- a/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template +++ b/neutron/services/vpn/device_drivers/template/openswan/ipsec.conf.template @@ -14,7 +14,11 @@ conn {{ipsec_site_connection.id}} auto={{ipsec_site_connection.initiator}} # NOTE:REQUIRED # [subnet] + {% if ipsec_site_connection.local_cidr -%} + leftsubnet={{ipsec_site_connection.local_cidr}} + {% else -%} leftsubnet={{vpnservice.subnet.cidr}} + {% endif -%} # leftsubnet=networkA/netmaskA, networkB/netmaskB (IKEv2 only) # [updown] # What "updown" script to run to adjust routing and/or firewalling when