metadata-ipv6: Router namespace

We push a v6 host route to make the guest send its metadata requests
in the direction of our router. We redirect it to haproxy which
mangles the headers and sends the request along to metadata-agent.

Apparently the supported list of dhcp options for dhcpv6 is quite
short in dnsmasq (cf. dnsmasq --help dhcp6) - not including anything
like classless-static-route for dhcpv4. So we must rely solely on
radvd to push host routes to the guest.

Metadata access over IPv6 is supposed to work both on dual-stack and
v6-only networks.

The following v6 subnet modes are supposed to work:

--ipv6-ra-mode slaac --ipv6-address-mode slaac
--ipv6-ra-mode dhcpv6-stateless --ipv6-address-mode dhcpv6-stateless
--ipv6-ra-mode dhcpv6-stateful --ipv6-address-mode dhcpv6-stateful

Change-Id: I28f2914b1b67659af2db7240eae730ac43daccd2
Partial-Bug: #1460177
This commit is contained in:
Bence Romsics 2020-03-27 16:37:26 +01:00
parent a0b18d553d
commit a1f4ee3ade
9 changed files with 144 additions and 60 deletions

View File

@ -20,6 +20,7 @@ from neutron_lib import constants as lib_constants
from neutron_lib.exceptions import l3 as l3_exc
from neutron_lib.utils import helpers
from oslo_log import log as logging
from oslo_utils import netutils
from pyroute2.netlink import exceptions as pyroute2_exc
from neutron._i18n import _
@ -1088,6 +1089,18 @@ class RouterInfo(BaseRouterInfo):
self.iptables_manager.ipv4['mangle'].add_rule(
'PREROUTING', mark_metadata_for_internal_interfaces)
if netutils.is_ipv6_enabled():
mark_metadata_v6_for_internal_interfaces = (
'-d fe80::a9fe:a9fe/128 '
'-i %(interface_name)s '
'-p tcp -m tcp --dport 80 '
'-j MARK --set-xmark %(value)s/%(mask)s' %
{'interface_name': INTERNAL_DEV_PREFIX + '+',
'value': self.agent_conf.metadata_access_mark,
'mask': lib_constants.ROUTER_MARK_MASK})
self.iptables_manager.ipv6['mangle'].add_rule(
'PREROUTING', mark_metadata_v6_for_internal_interfaces)
def _get_port_devicename_scopemark(self, ports, name_generator):
devicename_scopemark = {lib_constants.IP_VERSION_4: dict(),
lib_constants.IP_VERSION_6: dict()}

View File

@ -372,9 +372,12 @@ class IptablesManager(object):
def initialize_nat_table(self):
self.ipv4.update(
{'nat': IptablesTable(binary_name=self.wrap_name)})
self.ipv6.update(
{'nat': IptablesTable(binary_name=self.wrap_name)})
builtin_chains = {
4: {'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING']}}
4: {'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING']},
6: {'nat': ['PREROUTING']}}
self._configure_builtin_chains(builtin_chains)
# Add a neutron-postrouting-bottom chain. It's intended to be

View File

@ -73,6 +73,9 @@ CONFIG_TEMPLATE = jinja2.Template("""interface {{ interface_name }}
AdvAutonomous off;
};
{% endfor %}
route fe80::a9fe:a9fe/128 {
};
};
""")

View File

@ -16,6 +16,8 @@ import hashlib
import hmac
import urllib
import netaddr
from neutron_lib.agent import topics
from neutron_lib import constants
from neutron_lib import context
@ -168,7 +170,7 @@ class MetadataProxyHandler(object):
skip_cache=skip_cache)
def _get_instance_and_tenant_id(self, req, skip_cache=False):
remote_address = req.headers.get('X-Forwarded-For')
forwarded_for = req.headers.get('X-Forwarded-For')
network_id = req.headers.get('X-Neutron-Network-ID')
router_id = req.headers.get('X-Neutron-Router-ID')
@ -179,12 +181,19 @@ class MetadataProxyHandler(object):
"dropping")
return None, None
ports = self._get_ports(remote_address, network_id, router_id,
remote_ip = netaddr.IPAddress(forwarded_for)
if remote_ip.version == constants.IP_VERSION_6:
if remote_ip.is_ipv4_mapped():
# When haproxy listens on v4 AND v6 then it inserts ipv4
# addresses as ipv4-mapped v6 addresses into X-Forwarded-For.
forwarded_for = str(remote_ip.ipv4())
ports = self._get_ports(forwarded_for, network_id, router_id,
skip_cache=skip_cache)
LOG.debug("Gotten ports for remote_address %(remote_address)s, "
"network_id %(network_id)s, router_id %(router_id)s are: "
"%(ports)s",
{"remote_address": remote_address,
{"remote_address": forwarded_for,
"network_id": network_id,
"router_id": router_id,
"ports": ports})

View File

@ -25,10 +25,12 @@ from neutron_lib import constants
from neutron_lib import exceptions
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import netutils
from neutron._i18n import _
from neutron.agent.l3 import ha_router
from neutron.agent.l3 import namespaces
from neutron.agent.linux import dhcp
from neutron.agent.linux import external_process
from neutron.agent.linux import ip_lib
@ -205,12 +207,14 @@ class MetadataDriver(object):
'-j DROP' % port)]
@classmethod
def metadata_nat_rules(cls, port):
return [('PREROUTING', '-d 169.254.169.254/32 '
def metadata_nat_rules(
cls, port, metadata_address=(dhcp.METADATA_DEFAULT_IP + '/32')):
return [('PREROUTING', '-d %(metadata_address)s '
'-i %(interface_name)s '
'-p tcp -m tcp --dport 80 -j REDIRECT '
'--to-ports %(port)s' %
{'interface_name': namespaces.INTERNAL_DEV_PREFIX + '+',
{'metadata_address': metadata_address,
'interface_name': namespaces.INTERNAL_DEV_PREFIX + '+',
'port': port})]
@classmethod
@ -297,20 +301,34 @@ class MetadataDriver(object):
def after_router_added(resource, event, l3_agent, **kwargs):
router = kwargs['router']
proxy = l3_agent.metadata_driver
ipv6_enabled = netutils.is_ipv6_enabled()
for c, r in proxy.metadata_filter_rules(proxy.metadata_port,
proxy.metadata_access_mark):
router.iptables_manager.ipv4['filter'].add_rule(c, r)
if ipv6_enabled:
for c, r in proxy.metadata_filter_rules(proxy.metadata_port,
proxy.metadata_access_mark):
router.iptables_manager.ipv6['filter'].add_rule(c, r)
for c, r in proxy.metadata_nat_rules(proxy.metadata_port):
router.iptables_manager.ipv4['nat'].add_rule(c, r)
if ipv6_enabled:
for c, r in proxy.metadata_nat_rules(
proxy.metadata_port,
metadata_address=(dhcp.METADATA_V6_IP + '/128')):
router.iptables_manager.ipv6['nat'].add_rule(c, r)
router.iptables_manager.apply()
if not isinstance(router, ha_router.HaRouter):
spawn_kwargs = {}
if ipv6_enabled:
spawn_kwargs['bind_address'] = '::'
proxy.spawn_monitored_metadata_proxy(
l3_agent.process_monitor,
router.ns_name,
proxy.metadata_port,
l3_agent.conf,
router_id=router.router_id)
router_id=router.router_id,
**spawn_kwargs)
def after_router_updated(resource, event, l3_agent, **kwargs):
@ -318,12 +336,16 @@ def after_router_updated(resource, event, l3_agent, **kwargs):
proxy = l3_agent.metadata_driver
if (not proxy.monitors.get(router.router_id) and
not isinstance(router, ha_router.HaRouter)):
spawn_kwargs = {}
if netutils.is_ipv6_enabled():
spawn_kwargs['bind_address'] = '::'
proxy.spawn_monitored_metadata_proxy(
l3_agent.process_monitor,
router.ns_name,
proxy.metadata_port,
l3_agent.conf,
router_id=router.router_id)
router_id=router.router_id,
**spawn_kwargs)
def before_router_removed(resource, event, l3_agent, payload=None):

View File

@ -2664,28 +2664,31 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
'distributed': False}
driver = metadata_driver.MetadataDriver
with mock.patch.object(
driver, 'destroy_monitored_metadata_proxy') as destroy_proxy:
with mock.patch.object(
driver, 'spawn_monitored_metadata_proxy') as spawn_proxy:
agent._process_added_router(router)
if enableflag:
spawn_proxy.assert_called_with(
mock.ANY,
mock.ANY,
self.conf.metadata_port,
mock.ANY,
router_id=router_id
)
else:
self.assertFalse(spawn_proxy.call_count)
agent._safe_router_removed(router_id)
if enableflag:
destroy_proxy.assert_called_with(mock.ANY,
router_id,
mock.ANY,
'qrouter-' + router_id)
else:
self.assertFalse(destroy_proxy.call_count)
driver, 'destroy_monitored_metadata_proxy') as destroy_proxy, \
mock.patch.object(
driver, 'spawn_monitored_metadata_proxy') as spawn_proxy, \
mock.patch.object(netutils, 'is_ipv6_enabled') as ipv6_mock:
ipv6_mock.return_value = True
agent._process_added_router(router)
if enableflag:
spawn_proxy.assert_called_with(
mock.ANY,
mock.ANY,
self.conf.metadata_port,
mock.ANY,
router_id=router_id,
bind_address='::',
)
else:
self.assertFalse(spawn_proxy.call_count)
agent._safe_router_removed(router_id)
if enableflag:
destroy_proxy.assert_called_with(mock.ANY,
router_id,
mock.ANY,
'qrouter-' + router_id)
else:
self.assertFalse(destroy_proxy.call_count)
def test_enable_metadata_proxy(self):
self._configure_metadata_proxy()

View File

@ -314,6 +314,16 @@ def _generate_mangle_dump_v6(iptables_args):
'# Completed by iptables_manager\n' % iptables_args)
def _generate_nat_dump_v6(iptables_args):
return ('# Generated by iptables_manager\n'
'*nat\n'
':PREROUTING - [0:0]\n'
':%(bn)s-PREROUTING - [0:0]\n'
'-I PREROUTING 1 -j %(bn)s-PREROUTING\n'
'COMMIT\n'
'# Completed by iptables_manager\n' % iptables_args)
def _generate_raw_dump(iptables_args):
return ('# Generated by iptables_manager\n'
'*raw\n'
@ -366,6 +376,7 @@ def _generate_raw_restore_dump(iptables_args):
MANGLE_DUMP = _generate_mangle_dump(IPTABLES_ARG)
MANGLE_DUMP_V6 = _generate_mangle_dump_v6(IPTABLES_ARG)
NAT_DUMP_V6 = _generate_nat_dump_v6(IPTABLES_ARG)
RAW_DUMP = _generate_raw_dump(IPTABLES_ARG)
MANGLE_RESTORE_DUMP = _generate_mangle_restore_dump(IPTABLES_ARG)
RAW_RESTORE_DUMP = _generate_raw_restore_dump(IPTABLES_ARG)
@ -468,7 +479,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + RAW_DUMP)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + RAW_DUMP)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -512,7 +523,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + raw_dump)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + raw_dump)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -588,7 +599,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + raw_dump)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + raw_dump)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -654,7 +665,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + RAW_DUMP)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + RAW_DUMP)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -725,7 +736,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + raw_dump)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + raw_dump)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -787,7 +798,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + RAW_DUMP)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + RAW_DUMP)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -1164,7 +1175,7 @@ class IptablesManagerStateFulTestCase(IptablesManagerBaseTestCase):
if self.use_ipv6:
self._extend_with_ip6tables_filter_end(
expected_calls_and_values,
FILTER_DUMP + MANGLE_DUMP_V6 + RAW_DUMP)
FILTER_DUMP + MANGLE_DUMP_V6 + NAT_DUMP_V6 + RAW_DUMP)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -1227,9 +1238,10 @@ class IptablesManagerStateFulTestCaseCustomBinaryName(
]
if self.use_ipv6:
mangle_dump_v6 = _generate_mangle_dump_v6(iptables_args)
nat_dump_v6 = _generate_nat_dump_v6(iptables_args)
self._extend_with_ip6tables_filter(
expected_calls_and_values,
filter_dump_ipv6 + mangle_dump_v6 + raw_dump)
filter_dump_ipv6 + mangle_dump_v6 + nat_dump_v6 + raw_dump)
tools.setup_mock_calls(self.execute, expected_calls_and_values)
@ -1294,9 +1306,10 @@ class IptablesManagerStateFulTestCaseEmptyCustomBinaryName(
]
if self.use_ipv6:
mangle_dump_v6 = _generate_mangle_dump_v6(iptables_args)
nat_dump_v6 = _generate_nat_dump_v6(iptables_args)
self._extend_with_ip6tables_filter(
expected_calls_and_values,
filter_dump + mangle_dump_v6 + raw_dump)
filter_dump + mangle_dump_v6 + nat_dump_v6 + raw_dump)
tools.setup_mock_calls(self.execute, expected_calls_and_values)

View File

@ -14,6 +14,7 @@
from unittest import mock
import ddt
from neutron_lib import constants as n_const
import testtools
import webob
@ -102,6 +103,7 @@ class TestMetadataProxyHandlerRpc(TestMetadataProxyHandlerBase):
self.assertEqual(expected, ports)
@ddt.ddt
class _TestMetadataProxyHandlerCacheMixin(object):
def test_call(self):
@ -242,8 +244,8 @@ class _TestMetadataProxyHandlerCacheMixin(object):
self.assertRaises(TypeError, self.handler._get_ports, 'remote_address')
def _get_instance_and_tenant_id_helper(self, headers, list_ports_retval,
networks=None, router_id=None):
remote_address = '192.168.1.1'
networks=None, router_id=None,
remote_address='192.168.1.1'):
headers['X-Forwarded-For'] = remote_address
req = mock.Mock(headers=headers)
@ -279,7 +281,8 @@ class _TestMetadataProxyHandlerCacheMixin(object):
return (instance_id, tenant_id)
def test_get_instance_id_router_id(self):
@ddt.data('192.168.1.1', '::ffff:192.168.1.1')
def test_get_instance_id_router_id(self, remote_address):
router_id = 'the_id'
headers = {
'X-Neutron-Router-ID': router_id
@ -294,12 +297,13 @@ class _TestMetadataProxyHandlerCacheMixin(object):
self.assertEqual(
('device_id', 'tenant_id'),
self._get_instance_and_tenant_id_helper(headers, ports,
networks=networks,
router_id=router_id)
self._get_instance_and_tenant_id_helper(
headers, ports, networks=networks, router_id=router_id,
remote_address=remote_address)
)
def test_get_instance_id_router_id_no_match(self):
@ddt.data('192.168.1.1', '::ffff:192.168.1.1')
def test_get_instance_id_router_id_no_match(self, remote_address):
router_id = 'the_id'
headers = {
'X-Neutron-Router-ID': router_id
@ -312,12 +316,13 @@ class _TestMetadataProxyHandlerCacheMixin(object):
]
self.assertEqual(
(None, None),
self._get_instance_and_tenant_id_helper(headers, ports,
networks=networks,
router_id=router_id)
self._get_instance_and_tenant_id_helper(
headers, ports, networks=networks, router_id=router_id,
remote_address=remote_address)
)
def test_get_instance_id_network_id(self):
@ddt.data('192.168.1.1', '::ffff:192.168.1.1')
def test_get_instance_id_network_id(self, remote_address):
network_id = 'the_id'
headers = {
'X-Neutron-Network-ID': network_id
@ -331,11 +336,13 @@ class _TestMetadataProxyHandlerCacheMixin(object):
self.assertEqual(
('device_id', 'tenant_id'),
self._get_instance_and_tenant_id_helper(headers, ports,
networks=('the_id',))
self._get_instance_and_tenant_id_helper(
headers, ports, networks=('the_id',),
remote_address=remote_address)
)
def test_get_instance_id_network_id_no_match(self):
@ddt.data('192.168.1.1', '::ffff:192.168.1.1')
def test_get_instance_id_network_id_no_match(self, remote_address):
network_id = 'the_id'
headers = {
'X-Neutron-Network-ID': network_id
@ -345,11 +352,14 @@ class _TestMetadataProxyHandlerCacheMixin(object):
self.assertEqual(
(None, None),
self._get_instance_and_tenant_id_helper(headers, ports,
networks=('the_id',))
self._get_instance_and_tenant_id_helper(
headers, ports, networks=('the_id',),
remote_address=remote_address)
)
def test_get_instance_id_network_id_and_router_id_invalid(self):
@ddt.data('192.168.1.1', '::ffff:192.168.1.1')
def test_get_instance_id_network_id_and_router_id_invalid(
self, remote_address):
network_id = 'the_nid'
router_id = 'the_rid'
headers = {
@ -366,9 +376,9 @@ class _TestMetadataProxyHandlerCacheMixin(object):
self.assertEqual(
(None, None),
self._get_instance_and_tenant_id_helper(headers, ports,
networks=(network_id,),
router_id=router_id)
self._get_instance_and_tenant_id_helper(
headers, ports, networks=(network_id,), router_id=router_id,
remote_address=remote_address)
)
def _proxy_request_test_helper(self, response_code=200, method='GET'):

View File

@ -44,6 +44,14 @@ class TestMetadataDriverRules(base.BaseTestCase):
[rules],
metadata_driver.MetadataDriver.metadata_nat_rules(9697))
def test_metadata_nat_rules_ipv6(self):
rules = ('PREROUTING', '-d fe80::a9fe:a9fe/128 -i qr-+ '
'-p tcp -m tcp --dport 80 -j REDIRECT --to-ports 9697')
self.assertEqual(
[rules],
metadata_driver.MetadataDriver.metadata_nat_rules(
9697, metadata_address='fe80::a9fe:a9fe/128'))
def test_metadata_filter_rules(self):
rules = [('INPUT', '-m mark --mark 0x1/%s -j ACCEPT' %
constants.ROUTER_MARK_MASK),