--- /dev/null
+From 4dde0ffdddeaddef219d0ff6d131d474ec190167 Mon Sep 17 00:00:00 2001
+From: Sylvain Afchain <sylvain.afchain@enovance.com>
+Date: Thu, 12 Dec 2013 23:20:17 +0100
+Subject: [PATCH] Add parameter and iptables rules to protect dnsmasq ports
+
+Add a paramater to the configuration file of the dhcp
+agent. With this new param set as true, iptables rules
+are inserted to limit dns requests to dnsmasq only to
+the clients which are on the same subnet.
+
+Change-Id: Iac3635326cb81d5de51b903510ff31cb6164aa86
+Closes-bug: #1260731
+---
+ etc/dhcp_agent.ini | 5 +
+ neutron/agent/linux/dhcp.py | 159 ++++++++++++++++++++
+ neutron/tests/unit/test_linux_dhcp.py | 259 ++++++++++++++++++++++++++++++++-
+ 3 files changed, 421 insertions(+), 2 deletions(-)
+
+diff --git a/etc/dhcp_agent.ini b/etc/dhcp_agent.ini
+index 9836d35..df11b35 100644
+--- a/etc/dhcp_agent.ini
++++ b/etc/dhcp_agent.ini
+@@ -86,3 +86,8 @@
+ # Timeout for ovs-vsctl commands.
+ # If the timeout expires, ovs commands will fail with ALARMCLOCK error.
+ # ovs_vsctl_timeout = 10
++
++# Limits the dns requests to dnsmasq only to clients which are on its subnet.
++# Useful when a subnet is routed to another one or in the case of an
++# external network.
++# isolate_dns_requests = False
+diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py
+index e650c00..267e020 100644
+--- a/neutron/agent/linux/dhcp.py
++++ b/neutron/agent/linux/dhcp.py
+@@ -29,6 +29,7 @@ from oslo.config import cfg
+ import six
+
+ from neutron.agent.linux import ip_lib
++from neutron.agent.linux import iptables_manager
+ from neutron.agent.linux import utils
+ from neutron.common import constants
+ from neutron.common import exceptions
+@@ -59,6 +60,9 @@ OPTS = [
+ 'dnsmasq_lease_max',
+ default=(2 ** 24),
+ help=_('Limit number of leases to prevent a denial-of-service.')),
++ cfg.BoolOpt('isolate_dns_requests', default=False,
++ help=_("Allow dns requests to dnsmasq only from clients "
++ "on its subnet.")),
+ ]
+
+ IPV4 = 4
+@@ -75,6 +79,7 @@ METADATA_DEFAULT_CIDR = '%s/%d' % (METADATA_DEFAULT_IP,
+ METADATA_PORT = 80
+ WIN2k3_STATIC_DNS = 249
+ NS_PREFIX = 'qdhcp-'
++DNS_CHAIN_PREFIX = 'dns-'
+
+
+ class DictModel(object):
+@@ -670,7 +675,67 @@ class Dnsmasq(DhcpLocalProcess):
+ sock.close()
+
+
++class NamespaceIptablesManagerCache(object):
++ def __init__(self):
++ self.iptables_managers = {}
++ self.subnets = {}
++
++ def get_or_create(self, network, root_helper):
++ ns = network.namespace
++
++ im = self.iptables_managers.get(ns)
++ if not im:
++ im = iptables_manager.IptablesManager(
++ root_helper=root_helper,
++ use_ipv6=True,
++ namespace=network.namespace)
++ self.iptables_managers[ns] = im
++
++ return im
++
++ def is_up_to_date(self, network):
++ subnets = self.subnets.get(network.id)
++ current_subnets = set([subnet.id for subnet in network.subnets])
++ return subnets == current_subnets
++
++ def get(self, network):
++ return self.iptables_managers.get(network.namespace)
++
++ def remove(self, network):
++ self.subnets.pop(network.id, None)
++ self.iptables_managers.pop(network.namespace, None)
++
++ def apply(self, network):
++ im = self.iptables_managers.get(network.namespace)
++ if not im:
++ LOG.warn(_("apply called on a non existant iptables_manager "
++ "for network %s, get_or_create has to be called "
++ "before"), network.id)
++ return
++
++ self.subnets[network.id] = set([subnet.id
++ for subnet in network.subnets])
++
++ im.apply()
++
++
++class InterfaceNameCache(object):
++ def __init__(self):
++ self.interface_names = {}
++
++ def set(self, network, interface_name):
++ self.interface_names[network.id] = interface_name
++
++ def get(self, network):
++ return self.interface_names.get(network.id)
++
++ def remove(self, network):
++ self.interface_names.pop(network.id, None)
++
++
+ class DeviceManager(object):
++ iptables_manager_cache = NamespaceIptablesManagerCache()
++ interface_name_cache = InterfaceNameCache()
+
+ def __init__(self, conf, root_helper, plugin):
+ self.conf = conf
+@@ -811,10 +876,93 @@ class DeviceManager(object):
+
+ return dhcp_port
+
++ @staticmethod
++ def _get_isolation_rule(proto, chain, interface):
++ return ('-p %(proto)s -m %(proto)s '
++ '--dport %(port)s -i %(interface)s '
++ '-j $%(chain)s') % {'port': DNS_PORT,
++ 'proto': proto,
++ 'chain': chain,
++ 'interface': interface}
++
++ def _remove_dns_isolation(self, network):
++ interface_name = DeviceManager.interface_name_cache.get(network)
++ if not interface_name:
++ LOG.warn(_("Error unable to get interface name for network: %s"),
++ network.id)
++ return
++
++ im = DeviceManager.iptables_manager_cache.get(network)
++ if not im:
++ LOG.error(_("Error unable get the iptables manager created "
++ "for network %s"), network.id)
++ return
++
++ rules_chain = iptables_manager.get_chain_name(DNS_CHAIN_PREFIX +
++ network.id)
++
++ for tables in [im.ipv4, im.ipv6]:
++ tables['filter'].remove_chain(rules_chain)
++
++ im.apply()
++
++ DeviceManager.iptables_manager_cache.remove(network)
++
++ def _apply_dns_isolation(self, network, interface_name=None):
++ if DeviceManager.iptables_manager_cache.is_up_to_date(
++ network):
++ return
++
++ im = DeviceManager.iptables_manager_cache.get_or_create(
++ network, self.root_helper)
++ if not im:
++ LOG.error(_("Error unable to create or get an iptables manager "
++ "for network %s"), network.id)
++ return
++
++ if not interface_name:
++ interface_name = DeviceManager.interface_name_cache.get(network)
++ if not interface_name:
++ LOG.error(_("Error unable to get interface name for network: %s"),
++ network.id)
++ return
++
++ rules_chain = iptables_manager.get_chain_name(DNS_CHAIN_PREFIX +
++ network.id)
++
++ for tables in [im.ipv4, im.ipv6]:
++ tables['filter'].add_chain(rules_chain)
++
++ # empty_chain has to be called since a previous subnet
++ # could be now removed, with an empty chain only current subnet
++ # rules will be present.
++ tables['filter'].empty_chain(rules_chain)
++ tables['filter'].add_rule(rules_chain, '-j DROP')
++
++ for proto in [UDP, TCP]:
++ rule = self._get_isolation_rule(proto, rules_chain,
++ interface_name)
++ tables['filter'].add_rule('INPUT', rule)
++
++ # allow traffic from subnets everything else will be denied
++ for subnet in network.subnets:
++ if not subnet.enable_dhcp:
++ continue
++
++ tables = im.ipv4 if subnet.ip_version == IPV4 else im.ipv6
++
++ rule = '-s ' + subnet.cidr + ' -j RETURN'
++ tables['filter'].add_rule(rules_chain,
++ rule, top=True)
++
++ DeviceManager.iptables_manager_cache.apply(network)
++
+ def setup(self, network, reuse_existing=False):
+ """Create and initialize a device for network's DHCP on this host."""
+ port = self.setup_dhcp_port(network)
++
+ interface_name = self.get_interface_name(network, port)
++ DeviceManager.interface_name_cache.set(network, interface_name)
+
+ if ip_lib.device_exists(interface_name,
+ self.root_helper,
+@@ -853,6 +1001,9 @@ class DeviceManager(object):
+ if self.conf.use_namespaces:
+ self._set_default_route(network, port)
+
++ if self.conf.isolate_dns_requests:
++ self._apply_dns_isolation(network, interface_name)
++
+ return interface_name
+
+ def update(self, network):
+@@ -864,9 +1015,17 @@ class DeviceManager(object):
+ raise exceptions.NetworkNotFound(net_id=network.id)
+ self._set_default_route(network, port)
+
++ if self.conf.isolate_dns_requests:
++ self._apply_dns_isolation(network)
++
+ def destroy(self, network, device_name):
+ """Destroy the device used for the network's DHCP on this host."""
++ if self.conf.isolate_dns_requests:
++ self._remove_dns_isolation(network)
++
+ self.driver.unplug(device_name, namespace=network.namespace)
+
+ self.plugin.release_dhcp_port(network.id,
+ self.get_device_id(network))
++
++ DeviceManager.interface_name_cache.remove(network)
+diff --git a/neutron/tests/unit/test_linux_dhcp.py b/neutron/tests/unit/test_linux_dhcp.py
+index 7764bce..f18677f 100644
+--- a/neutron/tests/unit/test_linux_dhcp.py
++++ b/neutron/tests/unit/test_linux_dhcp.py
+@@ -15,6 +15,7 @@
+ # License for the specific language governing permissions and limitations
+ # under the License.
+
++import contextlib
+ import os
+
+ import mock
+@@ -396,8 +397,8 @@ class TestBase(base.BaseTestCase):
+ self.conf.register_opts(base_config.core_opts)
+ self.conf.register_opts(dhcp.OPTS)
+ config.register_interface_driver_opts_helper(self.conf)
+- instance = mock.patch("neutron.agent.linux.dhcp.DeviceManager")
+- self.mock_mgr = instance.start()
++ self.device_mock = mock.patch("neutron.agent.linux.dhcp.DeviceManager")
++ self.mock_mgr = self.device_mock.start()
+ self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
+ default=True))
+ self.conf(args=args)
+@@ -531,6 +532,260 @@ class TestDhcpLocalProcess(TestBase):
+ self.assertEqual(lp.called, ['spawn'])
+ self.assertTrue(mocks['interface_name'].__set__.called)
+
++ def _test_namespace_iptables_manager_cache(self):
++ network1 = FakeV4Network()
++ network2 = FakeV4Network()
++ network2.id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
++ network2.namespace = 'abc'
++ network3 = FakeV4Network()
++ network3.id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
++ network3.namespace = 'ghi'
++
++ im_cache = dhcp.NamespaceIptablesManagerCache()
++ im1 = im_cache.get_or_create(network1, 'sudo')
++ self.assertIsNotNone(im1)
++
++ im = im_cache.get_or_create(network1, 'sudo')
++ self.assertEqual(im, im1)
++
++ im2 = im_cache.get_or_create(network2, 'sudo')
++ self.assertIsNotNone(im2)
++ self.assertNotEqual(im1, im2)
++
++ im = im_cache.get(network1)
++ self.assertEqual(im, im1)
++
++ im = im_cache.get(network2)
++ self.assertEqual(im, im2)
++
++ im = im_cache.get(network3)
++ self.assertIsNone(im)
++
++ im_cache.remove(network1)
++ im = im_cache.get(network1)
++ self.assertIsNone(im)
++
++ iptables_inst = mock.Mock()
++ with contextlib.nested(mock.patch('neutron.agent.linux.'
++ 'iptables_manager.IptablesManager',
++ return_value=iptables_inst),
++ mock.patch.object(dhcp.LOG, 'error')
++ ) as (ipm, log):
++ im_cache = dhcp.NamespaceIptablesManagerCache()
++ up_to_date = im_cache.is_up_to_date(network1)
++ self.assertFalse(up_to_date)
++
++ im = im_cache.get_or_create(network1, 'sudo')
++ im_cache.apply(network1)
++
++ up_to_date = im_cache.is_up_to_date(network1)
++ self.assertTrue(up_to_date)
++
++ network1.subnets.append(FakeV4SubnetNoRouter())
++ up_to_date = im_cache.is_up_to_date(network1)
++ self.assertFalse(up_to_date)
++
++ im_cache.apply(network3)
++ log.assert_called_only_once()
++
++ def test_interface_name_cache(self):
++ int_cache = dhcp.InterfaceNameCache()
++
++ network = FakeV4Network()
++ int_cache.set(network, 'tap123')
++ name = int_cache.get(network)
++ self.assertEqual('tap123', name)
++
++ name = int_cache.get(FakeV4SubnetNoRouter())
++ self.assertEqual(name, None)
++
++ int_cache.remove(network)
++ name = int_cache.get(network)
++ self.assertEqual(name, None)
++
++ def test_apply_dns_isolation_non_existing_port(self):
++ self.device_mock.stop()
++ self.conf.set_override('interface_driver',
++ 'neutron.agent.linux.interface.'
++ 'OVSInterfaceDriver')
++
++ iptables_inst = mock.Mock()
++ v4filter_inst = mock.Mock()
++ v6filter_inst = mock.Mock()
++ v4filter_inst.chains = []
++ v6filter_inst.chains = []
++ iptables_inst.ipv4 = {'filter': v4filter_inst}
++ iptables_inst.ipv6 = {'filter': v6filter_inst}
++
++ plugin_inst = mock.Mock()
++
++ dhcp.DeviceManager.interface_name_cache = dhcp.InterfaceNameCache()
++ dhcp.DeviceManager.iptables_manager_cache = (
++ dhcp.NamespaceIptablesManagerCache())
++
++ with contextlib.nested(mock.patch('neutron.agent.linux.interface.'
++ 'OVSInterfaceDriver'),
++ mock.patch('neutron.agent.linux.'
++ 'iptables_manager.IptablesManager',
++ return_value=iptables_inst),
++ mock.patch.object(dhcp.LOG, 'error')
++ ) as (ovs_driver, ipm, log):
++ network = FakeV4Network()
++
++ plugin_inst.get_dhcp_port.return_value = None
++
++ device_mng = dhcp.DeviceManager(self.conf,
++ 'sudo', plugin_inst)
++ device_mng.driver.get_device_name.return_value = 'tap123'
++ device_mng.get_interface_name(network, network.ports[0])
++
++ log.assert_called_once()
++
++ self.assertFalse(v4filter_inst.called)
++ self.assertFalse(v6filter_inst.called)
++ self.assertFalse(iptables_inst.apply.called)
++
++ def test_apply_dns_isolation(self):
++ self.device_mock.stop()
++ self.conf.set_override('interface_driver',
++ 'neutron.agent.linux.interface.'
++ 'OVSInterfaceDriver')
++
++ iptables_inst = mock.Mock()
++ v4filter_inst = mock.Mock()
++ v6filter_inst = mock.Mock()
++ v4filter_inst.chains = []
++ v6filter_inst.chains = []
++ iptables_inst.ipv4 = {'filter': v4filter_inst}
++ iptables_inst.ipv6 = {'filter': v6filter_inst}
++
++ plugin_inst = mock.Mock()
++
++ dhcp.DeviceManager.interface_name_cache = dhcp.InterfaceNameCache()
++ dhcp.DeviceManager.iptables_manager_cache = (
++ dhcp.NamespaceIptablesManagerCache())
++
++ with contextlib.nested(mock.patch('neutron.agent.linux.interface.'
++ 'OVSInterfaceDriver'),
++ mock.patch('neutron.agent.linux.'
++ 'iptables_manager.IptablesManager',
++ return_value=iptables_inst)
++ ) as (ovs_driver, ipm):
++ network = FakeV4Network()
++
++ device_mng = dhcp.DeviceManager(self.conf,
++ 'sudo', plugin_inst)
++ device_mng.interface_name_cache.set(network, 'tap123')
++
++ device_mng._apply_dns_isolation(network)
++
++ callsv4 = [mock.call.add_chain('dns-aaaaaaa'),
++ mock.call.empty_chain('dns-aaaaaaa'),
++ mock.call.add_rule('dns-aaaaaaa', '-j DROP'),
++ mock.call.add_rule('INPUT', '-p udp -m udp '
++ '--dport 53 -i tap123 '
++ '-j $dns-aaaaaaa'),
++ mock.call.add_rule('INPUT', '-p tcp -m tcp '
++ '--dport 53 -i tap123 '
++ '-j $dns-aaaaaaa'),
++ mock.call.add_rule('dns-aaaaaaa',
++ '-s 192.168.0.0/24 '
++ '-j RETURN', top=True)]
++ v4filter_inst.assert_has_calls(callsv4)
++
++ callsv6 = [mock.call.add_chain('dns-aaaaaaa'),
++ mock.call.empty_chain('dns-aaaaaaa'),
++ mock.call.add_rule('dns-aaaaaaa', '-j DROP'),
++ mock.call.add_rule('INPUT', '-p udp -m udp '
++ '--dport 53 -i tap123 '
++ '-j $dns-aaaaaaa'),
++ mock.call.add_rule('INPUT', '-p tcp -m tcp '
++ '--dport 53 -i tap123 '
++ '-j $dns-aaaaaaa')]
++ v6filter_inst.assert_has_calls(callsv6)
++
++ iptables_inst.apply.assert_called_once()
++
++ v4filter_inst.reset_mock()
++ v6filter_inst.reset_mock()
++
++ network = FakeV6Network()
++
++ device_mng.interface_name_cache.set(network, 'tap123')
++ device_mng._apply_dns_isolation(network)
++
++ callsv4 = [mock.call.add_chain('dns-bbbbbbb'),
++ mock.call.empty_chain('dns-bbbbbbb'),
++ mock.call.add_rule('dns-bbbbbbb', '-j DROP'),
++ mock.call.add_rule('INPUT', '-p udp -m udp '
++ '--dport 53 -i tap123 '
++ '-j $dns-bbbbbbb'),
++ mock.call.add_rule('INPUT', '-p tcp -m tcp '
++ '--dport 53 -i tap123 '
++ '-j $dns-bbbbbbb')]
++ v4filter_inst.assert_has_calls(callsv4)
++
++ callsv6 = [mock.call.add_chain('dns-bbbbbbb'),
++ mock.call.empty_chain('dns-bbbbbbb'),
++ mock.call.add_rule('dns-bbbbbbb', '-j DROP'),
++ mock.call.add_rule('INPUT', '-p udp -m udp '
++ '--dport 53 -i tap123 '
++ '-j $dns-bbbbbbb'),
++ mock.call.add_rule('INPUT', '-p tcp -m tcp '
++ '--dport 53 -i tap123 '
++ '-j $dns-bbbbbbb'),
++ mock.call.add_rule('dns-bbbbbbb',
++ '-s fdca:3ba5:a17a:4ba3::/64'
++ ' -j RETURN', top=True)]
++ v6filter_inst.assert_has_calls(callsv6)
++
++ iptables_inst.apply.assert_called_once()
++
++ def test_remove_dns_isolation(self):
++ self.device_mock.stop()
++ self.conf.set_override('interface_driver',
++ 'neutron.agent.linux.interface.'
++ 'OVSInterfaceDriver')
++
++ iptables_inst = mock.Mock()
++ v4filter_inst = mock.Mock()
++ v6filter_inst = mock.Mock()
++ v4filter_inst.chains = []
++ v6filter_inst.chains = []
++ iptables_inst.ipv4 = {'filter': v4filter_inst}
++ iptables_inst.ipv6 = {'filter': v6filter_inst}
++
++ plugin_inst = mock.Mock()
++
++ dhcp.DeviceManager.interface_name_cache = dhcp.InterfaceNameCache()
++ dhcp.DeviceManager.iptables_manager_cache = (
++ dhcp.NamespaceIptablesManagerCache())
++
++ with contextlib.nested(mock.patch('neutron.agent.linux.interface.'
++ 'OVSInterfaceDriver'),
++ mock.patch('neutron.agent.linux.'
++ 'iptables_manager.IptablesManager',
++ return_value=iptables_inst)
++ ) as (ovs_driver, ipm):
++ network = FakeV4Network()
++
++ device_mng = dhcp.DeviceManager(self.conf,
++ 'sudo', plugin_inst)
++ device_mng.interface_name_cache.set(network, 'tap123')
++
++ # First, apply the dns isolation
++ device_mng._apply_dns_isolation(network)
++
++ # Get a new instance of the DeviceManager, in order to check
++ # that the iptables manager and the interface name caches work.
++ device_mng = dhcp.DeviceManager(self.conf,
++ 'sudo', plugin_inst)
++ device_mng._remove_dns_isolation(network)
++
++ v4filter_inst.remove_chain.assert_called_once_with('dns-aaaaaaa')
++
++ iptables_inst.apply.assert_called_once()
++
+ def test_disable_not_active(self):
+ attrs_to_mock = dict([(a, mock.DEFAULT) for a in
+ ['active', 'interface_name', 'pid']])
+--
+1.7.9.5
+