Skip to content

Commit dcef25c

Browse files
Ihar Hrachyshkabrianphaley
authored andcommitted
Support nested SNAT for ml2/ovn
When ovn_router_indirect_snat = True, ml2/ovn will set a catch-all snat rule for each external ip, instead of a snat rule per attached subnet. NB: This option is global to cluster and cannot be controlled per project or per router. NB2: this patch assumes that 0.0.0.0/0 snat rules are properly handled by OVN. Some (e.g. 22.03 and 24.03) OVN versions may have this scenario broken. See: https://issues.redhat.com/browse/FDP-744 for details. -- A long time ago, nested SNAT behavior was unconditionally enabled for ml2/ovs, see: https://bugs.launchpad.net/neutron/+bug/1386041 Since this behavior has potential security implications, and since it may not be desired in all environments, a new flag is introduced. Since OVN was deployed without nested SNAT enabled in multiple environments, the flag is set to False by default (meaning: no nested SNAT). In theory, instead of a config option, neutron could introduce a new API to allow users to control the behavior per router. This would require more work though. This granular API is left out of the patch. Interested parties are welcome to start a discussion about adding the new API as a new neutron extension to routers. -- Before this patch, there was an alternative implementation proposed that was not relying on 0.0.0.0/0 snat behavior implemented properly in OVN. The implementation was abandoned because it introduced non-negligible complexity in the neutron code and the OVN NB database. See: https://review.opendev.org/c/openstack/neutron/+/907504 -- Conflicts: neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py Closes-Bug: #2051935 Co-Authored-By: Brian Haley <[email protected]> Change-Id: I28fae44edc122fae389916e25b3321550de001fd (cherry picked from commit dbf53b7)
1 parent 6a46bc0 commit dcef25c

File tree

8 files changed

+247
-110
lines changed

8 files changed

+247
-110
lines changed

neutron/common/ovn/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,6 @@
463463
portbindings.VNIC_BAREMETAL,
464464
portbindings.VNIC_VIRTIO_FORWARDER,
465465
]
466+
467+
# OVN default SNAT CIDR
468+
OVN_DEFAULT_SNAT_CIDR = '0.0.0.0/0'

neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@
221221
default=0,
222222
help=_('The number of seconds to keep MAC_Binding entries in '
223223
'the OVN DB. 0 to disable aging.')),
224+
cfg.BoolOpt('ovn_router_indirect_snat',
225+
default=False,
226+
help=_('Whether to configure SNAT for all nested subnets '
227+
'connected to the router through any other routers, '
228+
'similar to the default ML2/OVS behavior. Defaults to '
229+
'"False".')),
224230
]
225231

226232
nb_global_opts = [
@@ -376,3 +382,7 @@ def get_fdb_removal_limit():
376382
def get_ovn_mac_binding_age_threshold():
377383
# This value is always stored as a string in the OVN DB
378384
return str(cfg.CONF.ovn.mac_binding_age_threshold)
385+
386+
387+
def is_ovn_router_indirect_snat_enabled():
388+
return cfg.CONF.ovn.ovn_router_indirect_snat

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py

Lines changed: 90 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import collections
1717
import copy
1818
import datetime
19+
import functools
1920

2021
import netaddr
2122
from neutron_lib.api.definitions import l3
@@ -51,6 +52,8 @@
5152
from neutron.common import utils as common_utils
5253
from neutron.conf.agent import ovs_conf
5354
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
55+
from neutron.conf.plugins.ml2.drivers.ovn.ovn_conf \
56+
import is_ovn_router_indirect_snat_enabled as is_nested_snat
5457
from neutron.db import ovn_revision_numbers_db as db_rev
5558
from neutron.db import segments_db
5659
from neutron.objects import router
@@ -64,6 +67,10 @@
6467
LOG = log.getLogger(__name__)
6568

6669

70+
def _has_separate_snat_per_subnet(router):
71+
return utils.is_snat_enabled(router) and not is_nested_snat()
72+
73+
6774
OvnPortInfo = collections.namedtuple(
6875
"OvnPortInfo",
6976
[
@@ -1219,23 +1226,23 @@ def _get_gw_info(self, context, port_dict):
12191226
else const.IPv6_ANY))
12201227
return gateways_info
12211228

1222-
def _delete_router_ext_gw(self, router, networks, txn):
1229+
def _delete_router_ext_gw(self, router_id, txn):
12231230
context = n_context.get_admin_context()
1224-
if not networks:
1225-
networks = []
1226-
router_id = router['id']
1231+
cidrs = self._get_snat_cidrs_for_external_router(context, router_id)
12271232
gw_lrouter_name = utils.ovn_name(router_id)
12281233
deleted_ports = []
12291234
for gw_port in self._get_router_gw_ports(context, router_id):
12301235
for gw_info in self._get_gw_info(context, gw_port):
1231-
if gw_info.ip_version == const.IP_VERSION_4:
1232-
for network in networks:
1233-
txn.add(self._nb_idl.delete_nat_rule_in_lrouter(
1234-
gw_lrouter_name, type='snat', logical_ip=network,
1235-
external_ip=gw_info.router_ip))
12361236
txn.add(self._nb_idl.delete_static_route(
12371237
gw_lrouter_name, ip_prefix=gw_info.ip_prefix,
12381238
nexthop=gw_info.gateway_ip))
1239+
if gw_info.ip_version != const.IP_VERSION_4:
1240+
continue
1241+
for cidr in cidrs:
1242+
txn.add(self._nb_idl.delete_nat_rule_in_lrouter(
1243+
gw_lrouter_name, type='snat',
1244+
external_ip=gw_info.router_ip,
1245+
logical_ip=cidr))
12391246
txn.add(self._nb_idl.delete_lrouter_port(
12401247
utils.ovn_lrouter_port_name(gw_port['id']),
12411248
gw_lrouter_name))
@@ -1281,7 +1288,7 @@ def _get_nets_and_ipv6_ra_confs_for_router_port(self, context, port):
12811288

12821289
return list(networks), ipv6_ra_configs
12831290

1284-
def _add_router_ext_gw(self, context, router, networks, txn):
1291+
def _add_router_ext_gw(self, context, router, txn):
12851292
lrouter_name = utils.ovn_name(router['id'])
12861293
router_default_route_ecmp_enabled = router.get(
12871294
'enable_default_route_ecmp', False)
@@ -1319,9 +1326,9 @@ def _add_router_ext_gw(self, context, router, networks, txn):
13191326
maintain_bfd=router_default_route_bfd_enabled,
13201327
**columns))
13211328

1322-
# 3. Add snat rules for tenant networks in lrouter if snat is enabled
1323-
if utils.is_snat_enabled(router) and networks:
1324-
self.update_nat_rules(router, networks, enable_snat=True, txn=txn)
1329+
# 3. Add necessary snat rule(s) in lrouter if snat is enabled
1330+
if utils.is_snat_enabled(router):
1331+
self.update_nat_rules(router['id'], enable_snat=True, txn=txn)
13251332
return added_ports
13261333

13271334
def _check_external_ips_changed(self, ovn_snats,
@@ -1423,17 +1430,20 @@ def _get_v4_network_for_router_port(self, context, port):
14231430
cidr = subnet['cidr']
14241431
return cidr
14251432

1426-
def _get_v4_network_of_all_router_ports(self, context, router_id,
1427-
ports=None):
1433+
def _get_v4_network_of_all_router_ports(self, context, router_id):
14281434
networks = []
1429-
ports = ports or self._get_router_ports(context, router_id)
1430-
for port in ports:
1435+
for port in self._get_router_ports(context, router_id):
14311436
network = self._get_v4_network_for_router_port(context, port)
14321437
if network:
14331438
networks.append(network)
1434-
14351439
return networks
14361440

1441+
def _get_snat_cidrs_for_external_router(self, context, router_id):
1442+
if is_nested_snat():
1443+
return [ovn_const.OVN_DEFAULT_SNAT_CIDR]
1444+
# nat rule per attached subnet per external ip
1445+
return self._get_v4_network_of_all_router_ports(context, router_id)
1446+
14371447
def _gen_router_ext_ids(self, router):
14381448
return {
14391449
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY:
@@ -1462,12 +1472,9 @@ def create_router(self, context, router, add_external_gateway=True):
14621472
# by the ovn_db_sync.py script, remove it after the database
14631473
# synchronization work
14641474
if add_external_gateway:
1465-
networks = self._get_v4_network_of_all_router_ports(
1466-
context, router['id'])
1467-
if (router.get(l3_ext_gw_multihoming.EXTERNAL_GATEWAYS) and
1468-
networks is not None):
1475+
if router.get(l3_ext_gw_multihoming.EXTERNAL_GATEWAYS):
14691476
added_gw_ports = self._add_router_ext_gw(
1470-
context, router, networks, txn)
1477+
context, router, txn)
14711478

14721479
self._qos_driver.create_router(txn, router)
14731480

@@ -1495,7 +1502,6 @@ def update_router(self, context, new_router, router_object=None):
14951502
l3_ext_gw_multihoming.EXTERNAL_GATEWAYS)
14961503

14971504
ovn_snats = utils.get_lrouter_snats(ovn_router)
1498-
networks = self._get_v4_network_of_all_router_ports(context, router_id)
14991505
try:
15001506
check_rev_cmd = self._nb_idl.check_revision_number(
15011507
router_name, new_router, ovn_const.TYPE_ROUTERS)
@@ -1504,13 +1510,13 @@ def update_router(self, context, new_router, router_object=None):
15041510
if gateway_new and not gateway_old:
15051511
# Route gateway is set
15061512
added_gw_ports = self._add_router_ext_gw(
1507-
context, new_router, networks, txn)
1513+
context, new_router, txn)
15081514
elif gateway_old and not gateway_new:
15091515
# router gateway is removed
15101516
txn.add(self._nb_idl.delete_lrouter_ext_gw(router_name))
15111517
if router_object:
15121518
deleted_gw_port_ids = self._delete_router_ext_gw(
1513-
router_object, networks, txn)
1519+
router_object['id'], txn)
15141520
elif gateway_new and gateway_old:
15151521
# Check if external gateway has changed, if yes, delete
15161522
# the old gateway and add the new gateway
@@ -1528,16 +1534,16 @@ def update_router(self, context, new_router, router_object=None):
15281534
router_name))
15291535
if router_object:
15301536
deleted_gw_port_ids = self._delete_router_ext_gw(
1531-
router_object, networks, txn)
1537+
router_object['id'], txn)
15321538
added_gw_ports = self._add_router_ext_gw(
1533-
context, new_router, networks, txn)
1539+
context, new_router, txn)
15341540
else:
15351541
# Check if snat has been enabled/disabled and update
15361542
new_snat_state = utils.is_snat_enabled(new_router)
1537-
if bool(ovn_snats) != new_snat_state and networks:
1543+
if bool(ovn_snats) != new_snat_state:
15381544
self.update_nat_rules(
1539-
new_router, networks,
1540-
enable_snat=new_snat_state, txn=txn)
1545+
new_router['id'], enable_snat=new_snat_state,
1546+
txn=txn)
15411547

15421548
update = {'external_ids': self._gen_router_ext_ids(new_router)}
15431549
update['enabled'] = new_router.get('admin_state_up') or False
@@ -1785,26 +1791,26 @@ def create_router_port(self, context, router_id, router_interface):
17851791

17861792
gw_ports = self._get_router_gw_ports(context, router_id)
17871793
if gw_ports:
1788-
cidr = None
1789-
for fixed_ip in port['fixed_ips']:
1790-
subnet = self._plugin.get_subnet(context,
1791-
fixed_ip['subnet_id'])
1792-
if multi_prefix:
1793-
if 'subnet_id' in router_interface:
1794-
if subnet['id'] != router_interface['subnet_id']:
1795-
continue
1796-
if subnet['ip_version'] == const.IP_VERSION_4:
1797-
cidr = subnet['cidr']
1798-
17991794
if ovn_conf.is_ovn_emit_need_to_frag_enabled():
18001795
for gw_port in gw_ports:
18011796
provider_net = self._plugin.get_network(
18021797
context, gw_port['network_id'])
18031798
self.set_gateway_mtu(context, provider_net)
18041799

1805-
if utils.is_snat_enabled(router) and cidr:
1806-
self.update_nat_rules(router, networks=[cidr],
1807-
enable_snat=True, txn=txn)
1800+
if _has_separate_snat_per_subnet(router):
1801+
for fixed_ip in port['fixed_ips']:
1802+
subnet = self._plugin.get_subnet(
1803+
context, fixed_ip['subnet_id'])
1804+
if (multi_prefix and
1805+
'subnet_id' in router_interface and
1806+
subnet['id'] != router_interface['subnet_id']):
1807+
continue
1808+
if subnet['ip_version'] == const.IP_VERSION_4:
1809+
self.update_nat_rules(
1810+
router['id'], cidrs=[subnet['cidr']],
1811+
enable_snat=True, txn=txn)
1812+
break # TODO(ihar): handle multiple ipv4 ips?
1813+
18081814
if ovn_conf.is_ovn_distributed_floating_ip():
18091815
router_gw_ports = self._get_router_gw_ports(context,
18101816
router_id)
@@ -1937,19 +1943,17 @@ def delete_router_port(self, context, port_id, subnet_ids=None):
19371943
context, gw_port['network_id'])
19381944
self.set_gateway_mtu(context, provider_net, txn=txn)
19391945

1940-
cidr = None
1941-
for sid in subnet_ids:
1942-
try:
1943-
subnet = self._plugin.get_subnet(context, sid)
1944-
except n_exc.SubnetNotFound:
1945-
continue
1946-
if subnet['ip_version'] == const.IP_VERSION_4:
1947-
cidr = subnet['cidr']
1948-
break
1949-
1950-
if utils.is_snat_enabled(router) and cidr:
1951-
self.update_nat_rules(
1952-
router, networks=[cidr], enable_snat=False, txn=txn)
1946+
if _has_separate_snat_per_subnet(router):
1947+
for sid in subnet_ids:
1948+
try:
1949+
subnet = self._plugin.get_subnet(context, sid)
1950+
except n_exc.SubnetNotFound:
1951+
continue
1952+
if subnet['ip_version'] == const.IP_VERSION_4:
1953+
self.update_nat_rules(
1954+
router['id'], cidrs=[subnet['cidr']],
1955+
enable_snat=False, txn=txn)
1956+
break # TODO(ihar): handle multiple ipv4 ips?
19531957

19541958
if ovn_conf.is_ovn_distributed_floating_ip():
19551959
router_gw_ports = self._get_router_gw_ports(context, router_id)
@@ -1968,20 +1972,35 @@ def delete_router_port(self, context, port_id, subnet_ids=None):
19681972
db_rev.bump_revision(
19691973
context, port, ovn_const.TYPE_ROUTER_PORTS)
19701974

1971-
def update_nat_rules(self, router, networks, enable_snat, txn=None):
1972-
"""Update the NAT rules in a logical router."""
1975+
def _iter_ipv4_gw_addrs(self, context, router_id):
1976+
yield from (
1977+
gw_info.router_ip
1978+
for gw_port in self._get_router_gw_ports(context, router_id)
1979+
for gw_info in self._get_gw_info(context, gw_port)
1980+
if gw_info.ip_version != const.IP_VERSION_6
1981+
)
1982+
1983+
def update_nat_rules(self, router_id, enable_snat, cidrs=None, txn=None):
1984+
if enable_snat:
1985+
idl_func = self._nb_idl.add_nat_rule_in_lrouter
1986+
else:
1987+
idl_func = self._nb_idl.delete_nat_rule_in_lrouter
1988+
func = functools.partial(
1989+
idl_func, utils.ovn_name(router_id), type='snat')
1990+
19731991
context = n_context.get_admin_context()
1974-
func = (self._nb_idl.add_nat_rule_in_lrouter if enable_snat else
1975-
self._nb_idl.delete_nat_rule_in_lrouter)
1976-
gw_lrouter_name = utils.ovn_name(router['id'])
1977-
# Update NAT rules only for IPv4 subnets
1978-
commands = [func(gw_lrouter_name, type='snat', logical_ip=network,
1979-
external_ip=gw_info.router_ip)
1980-
for gw_port in self._get_router_gw_ports(context,
1981-
router['id'])
1982-
for gw_info in self._get_gw_info(context, gw_port)
1983-
if gw_info.ip_version != const.IP_VERSION_6
1984-
for network in networks]
1992+
cidrs = (
1993+
cidrs or
1994+
self._get_snat_cidrs_for_external_router(context, router_id)
1995+
)
1996+
commands = [
1997+
func(logical_ip=cidr, external_ip=router_ip)
1998+
for router_ip in self._iter_ipv4_gw_addrs(context, router_id)
1999+
for cidr in cidrs
2000+
]
2001+
if not commands:
2002+
return
2003+
19852004
self._transaction(commands, txn=txn)
19862005

19872006
def create_provnet_port(self, network_id, segment, txn=None):

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -540,12 +540,12 @@ def sync_routers_and_rports(self, ctx):
540540
if gw_info.ip_version == constants.IP_VERSION_6:
541541
continue
542542
if gw_info.router_ip and utils.is_snat_enabled(router):
543-
networks = self._ovn_client.\
544-
_get_v4_network_of_all_router_ports(
545-
ctx, router['id'])
546-
for network in networks:
543+
cidrs = self._ovn_client.\
544+
_get_snat_cidrs_for_external_router(ctx,
545+
router['id'])
546+
for cidr in cidrs:
547547
db_extends[router['id']]['snats'].append({
548-
'logical_ip': network,
548+
'logical_ip': cidr,
549549
'external_ip': gw_info.router_ip,
550550
'type': 'snat'})
551551

0 commit comments

Comments
 (0)