PXE Filter dnsmasq: blacklist unknown host

Unless one or more nodes are on introspection and
node_not_found_hook is not set a dhcp_hostsdir ignore
record for wildcard mac '*:*:*:*:*:*' is maintained.

The iptables filter driver blocks DHCP requests on the
Inspector interface unless one or more nodes are on
introspection and node_not_found_hook is not set.

This change brings the dnsmasq filter driver to parity
by implementing logic similar to the iptables driver.

Related: rhbz#1574672
Story: 2001970
Task: 16864
Change-Id: Ibdd2210ecb3833a0d91205a7919122b7c0576b9e
This commit is contained in:
Harald Jensås 2018-05-04 22:45:54 +02:00
parent 361fb091a5
commit 5e54e72136
3 changed files with 133 additions and 0 deletions

View File

@ -41,6 +41,19 @@ _EXCLUSIVE_WRITE_ATTEMPTS_DELAY = 0.01
_ROOTWRAP_COMMAND = 'sudo ironic-inspector-rootwrap {rootwrap_config!s}'
_MACBL_LEN = len('ff:ff:ff:ff:ff:ff,ignore\n')
_UNKNOWN_HOSTS_FILE = 'unknown_hosts_filter'
_BLACKLIST_UNKNOWN_HOSTS = '*:*:*:*:*:*,ignore\n'
_WHITELIST_UNKNOWN_HOSTS = '*:*:*:*:*:*\n'
def _should_enable_unknown_hosts():
"""Check whether we should enable DHCP for unknown hosts
We blacklist unknown hosts unless one or more nodes are on introspection
and node_not_found_hook is not set.
"""
return (node_cache.introspection_active() or
CONF.processing.node_not_found_hook is not None)
class DnsmasqFilter(pxe_filter.BaseFilter):
@ -78,6 +91,9 @@ class DnsmasqFilter(pxe_filter.BaseFilter):
# blacklist new ports that aren't being inspected
for mac in ironic_macs - (blacklist_macs | active_macs):
_blacklist_mac(mac)
_configure_unknown_hosts()
timestamp_end = timeutils.utcnow()
LOG.debug('The dnsmasq PXE filter was synchronized (took %s)',
timestamp_end - timestamp_start)
@ -186,6 +202,41 @@ def _exclusive_write_or_pass(path, buf):
return False
def _configure_unknown_hosts():
"""Manages a dhcp_hostsdir ignore/not-ignore record for unknown macs.
:param allow: If True unknown hosts are whitelisted. If False unknown hosts
are blacklisted.
:raises: FileNotFoundError in case the dhcp_hostsdir is invalid,
IOError in case the dhcp host unknown file isn't writable.
:returns: None.
"""
path = os.path.join(CONF.dnsmasq_pxe_filter.dhcp_hostsdir,
_UNKNOWN_HOSTS_FILE)
if _should_enable_unknown_hosts():
wildcard_filter = _WHITELIST_UNKNOWN_HOSTS
log_wildcard_filter = 'whitelist'
else:
wildcard_filter = _BLACKLIST_UNKNOWN_HOSTS
log_wildcard_filter = 'blacklist'
# Don't update if unknown hosts are already black/white-listed
try:
if os.stat(path).st_size == len(wildcard_filter):
return
except OSError as e:
if e.errno != os.errno.ENOENT:
raise
if _exclusive_write_or_pass(path, '%s' % wildcard_filter):
LOG.debug('A %s record for all unknown hosts using wildcard mac '
'created', log_wildcard_filter)
else:
LOG.warning('Failed to %s unknown hosts using wildcard mac; '
'retrying next periodic sync time', log_wildcard_filter)
def _blacklist_mac(mac):
"""Creates a dhcp_hostsdir ignore record for the MAC.

View File

@ -34,6 +34,21 @@ class DnsmasqTestBase(test_base.BaseTest):
self.driver = dnsmasq.DnsmasqFilter()
class TestShouldEnableUnknownHosts(DnsmasqTestBase):
def setUp(self):
super(TestShouldEnableUnknownHosts, self).setUp()
self.mock_introspection_active = self.useFixture(
fixtures.MockPatchObject(node_cache, 'introspection_active')).mock
def test_introspection_active(self):
self.mock_introspection_active.return_value = True
self.assertIs(True, dnsmasq._should_enable_unknown_hosts())
def test_introspection_not_active(self):
self.mock_introspection_active.return_value = False
self.assertIs(False, dnsmasq._should_enable_unknown_hosts())
class TestDnsmasqDriverAPI(DnsmasqTestBase):
def setUp(self):
super(TestDnsmasqDriverAPI, self).setUp()
@ -197,6 +212,40 @@ class TestMACHandlers(test_base.BaseTest):
fixtures.MockPatchObject(os, 'listdir')).mock
self.mock_remove = self.useFixture(
fixtures.MockPatchObject(os, 'remove')).mock
self.mock_log = self.useFixture(
fixtures.MockPatchObject(dnsmasq, 'LOG')).mock
self.mock_introspection_active = self.useFixture(
fixtures.MockPatchObject(node_cache, 'introspection_active')).mock
def test__whitelist_unknown_hosts(self):
self.mock_join.return_value = "%s/%s" % (self.dhcp_hostsdir,
dnsmasq._UNKNOWN_HOSTS_FILE)
self.mock_introspection_active.return_value = True
dnsmasq._configure_unknown_hosts()
self.mock_join.assert_called_once_with(self.dhcp_hostsdir,
dnsmasq._UNKNOWN_HOSTS_FILE)
self.mock__exclusive_write_or_pass.assert_called_once_with(
self.mock_join.return_value,
'%s' % dnsmasq._WHITELIST_UNKNOWN_HOSTS)
self.mock_log.debug.assert_called_once_with(
'A %s record for all unknown hosts using wildcard mac '
'created', 'whitelist')
def test__blacklist_unknown_hosts(self):
self.mock_join.return_value = "%s/%s" % (self.dhcp_hostsdir,
dnsmasq._UNKNOWN_HOSTS_FILE)
self.mock_introspection_active.return_value = False
dnsmasq._configure_unknown_hosts()
self.mock_join.assert_called_once_with(self.dhcp_hostsdir,
dnsmasq._UNKNOWN_HOSTS_FILE)
self.mock__exclusive_write_or_pass.assert_called_once_with(
self.mock_join.return_value,
'%s' % dnsmasq._BLACKLIST_UNKNOWN_HOSTS)
self.mock_log.debug.assert_called_once_with(
'A %s record for all unknown hosts using wildcard mac '
'created', 'blacklist')
def test__whitelist_mac(self):
dnsmasq._whitelist_mac(self.mac)
@ -261,6 +310,9 @@ class TestSync(DnsmasqTestBase):
fixtures.MockPatchObject(dnsmasq, '_whitelist_mac')).mock
self.mock__blacklist_mac = self.useFixture(
fixtures.MockPatchObject(dnsmasq, '_blacklist_mac')).mock
self.mock__configure_unknown_hosts = self.useFixture(
fixtures.MockPatchObject(dnsmasq, '_configure_unknown_hosts')).mock
self.mock_ironic = mock.Mock()
self.mock_utcnow = self.useFixture(
fixtures.MockPatchObject(dnsmasq.timeutils, 'utcnow')).mock
@ -283,6 +335,22 @@ class TestSync(DnsmasqTestBase):
self.mock_ironic.port.list.return_value = [
mock.Mock(address=address) for address in self.ironic_macs]
self.mock_active_macs.return_value = self.active_macs
self.mock_should_enable_unknown_hosts = self.useFixture(
fixtures.MockPatchObject(dnsmasq,
'_should_enable_unknown_hosts')).mock
self.mock_should_enable_unknown_hosts.return_value = True
def test__sync_enable_unknown_hosts(self):
self.mock_should_enable_unknown_hosts.return_value = True
self.driver._sync(self.mock_ironic)
self.mock__configure_unknown_hosts.assert_called_once_with()
def test__sync_not_enable_unknown_hosts(self):
self.mock_should_enable_unknown_hosts.return_value = False
self.driver._sync(self.mock_ironic)
self.mock__configure_unknown_hosts.assert_called_once_with()
def test__sync(self):
self.driver._sync(self.mock_ironic)
@ -296,6 +364,7 @@ class TestSync(DnsmasqTestBase):
fields=['address'])
self.mock_active_macs.assert_called_once_with()
self.mock__get_blacklist.assert_called_once_with()
self.mock__configure_unknown_hosts.assert_called_once_with()
self.mock_log.debug.assert_has_calls([
mock.call('Syncing the driver'),
mock.call('The dnsmasq PXE filter was synchronized (took %s)',

View File

@ -0,0 +1,13 @@
---
features:
- |
Adds wildcard ignore entry to ``dnsmasq`` PXE filter. When node
introspection is active, or if ``node_not_found_hook`` is set in the
configuration the ignore is removed from the wildcard entry. This ensures
that unknown nodes do not accidentally boot into the introspection image
when no node introspection is active.
This brings ``dnsmasq`` PXE filter driver feature parity with the
``iptables`` PXE filter driver, which uses a firewall rule to block any
DHCP request on the interface where Ironic Inspector's DHCP server is
listening.