Allow the backdoor to serve from a local unix domain socket

Local files can be made accessible to certain users vs random
ports which can be accessible to anyone on a machine so allow
using unix files as a way to start the eventlet backdoor (so that
user and group permissions common on unix are not lost).

To use this new type of files `socat` is needed (or other way
to interact with telnet over a unix domain socket).

For example (with the path at /tmp/my_special_socket):

  socat - UNIX-CONNECT:/tmp/my_special_socket

Depends-On: Ia2385879e09991102f8f305ec41dbb651a4374de

Change-Id: I7f25913168ebe5854f360db3d6310b72a56b2b4d
This commit is contained in:
Joshua Harlow
2016-02-02 16:17:10 -08:00
committed by Joshua Harlow
parent de959dc78f
commit db1fc249e6
3 changed files with 109 additions and 19 deletions

View File

@@ -24,7 +24,14 @@ help_for_backdoor_port = (
"chosen port is displayed in the service's log file.")
eventlet_backdoor_opts = [
cfg.StrOpt('backdoor_port',
help="Enable eventlet backdoor. %s" % help_for_backdoor_port)
help="Enable eventlet backdoor. %s" % help_for_backdoor_port),
cfg.StrOpt('backdoor_socket',
help="Enable eventlet backdoor, using the provided path"
" as a unix socket that can receive connections. This"
" option is mutually exclusive with 'backdoor_port' in"
" that only one should be be provided. If both are"
" provided then the existence of this option overrides"
" the usage of that option.")
]
periodic_opts = [

View File

@@ -133,6 +133,27 @@ def _listen(host, start_port, end_port, listen_func):
try_port += 1
def _try_open_unix_domain_socket(socket_path):
try:
return eventlet.listen(socket_path, socket.AF_UNIX)
except socket.error as e:
if e.errno != errno.EADDRINUSE:
# NOTE(harlowja): Some other non-address in use error
# occurred, since we aren't handling those, re-raise
# and give up...
raise
else:
# Attempt to remove the file before opening it again.
try:
os.unlink(socket_path)
except OSError as e:
if e.errno != errno.ENOENT:
# NOTE(harlowja): File existed, but we couldn't
# delete it, give up...
raise
return eventlet.listen(socket_path, socket.AF_UNIX)
def _initialize_if_enabled(conf):
conf.register_opts(_options.eventlet_backdoor_opts)
backdoor_locals = {
@@ -143,10 +164,18 @@ def _initialize_if_enabled(conf):
'pnt': _print_nativethreads,
}
if conf.backdoor_port is None:
if conf.backdoor_port is None and conf.backdoor_socket is None:
return None
start_port, end_port = _parse_port_range(str(conf.backdoor_port))
if conf.backdoor_socket is None:
start_port, end_port = _parse_port_range(str(conf.backdoor_port))
sock = _listen('localhost', start_port, end_port, eventlet.listen)
# In the case of backdoor port being zero, a port number is assigned by
# listen(). In any case, pull the port number out here.
where_running = sock.getsockname()[1]
else:
sock = _try_open_unix_domain_socket(conf.backdoor_socket)
where_running = conf.backdoor_socket
# NOTE(johannes): The standard sys.displayhook will print the value of
# the last expression and set it to __builtin__._, which overwrites
@@ -158,27 +187,23 @@ def _initialize_if_enabled(conf):
pprint.pprint(val)
sys.displayhook = displayhook
sock = _listen('localhost', start_port, end_port, eventlet.listen)
# In the case of backdoor port being zero, a port number is assigned by
# listen(). In any case, pull the port number out here.
port = sock.getsockname()[1]
LOG.info(
_LI('Eventlet backdoor listening on %(port)s for process %(pid)d'),
{'port': port, 'pid': os.getpid()}
_LI('Eventlet backdoor listening on %(where_running)s for'
' process %(pid)d'),
{'where_running': where_running, 'pid': os.getpid()}
)
thread = eventlet.spawn(eventlet.backdoor.backdoor_server, sock,
locals=backdoor_locals)
return (port, thread)
return (where_running, thread)
def initialize_if_enabled(conf):
port_thread = _initialize_if_enabled(conf)
if not port_thread:
where_running_thread = _initialize_if_enabled(conf)
if not where_running_thread:
return None
else:
port, _thread = port_thread
return port
where_running, _thread = where_running_thread
return where_running
def _main():
@@ -193,11 +218,11 @@ def _main():
conf.register_cli_opts(_options.eventlet_backdoor_opts)
conf(sys.argv[1:])
port_thread = _initialize_if_enabled(conf)
if not port_thread:
raise RuntimeError("Did not create backdoor at requested port")
where_running_thread = _initialize_if_enabled(conf)
if not where_running_thread:
raise RuntimeError("Did not create backdoor at requested location")
else:
_port, thread = port_thread
_where_running, thread = where_running_thread
thread.wait()

View File

@@ -18,6 +18,7 @@
Unit Tests for eventlet backdoor
"""
import errno
import os
import socket
import eventlet
@@ -27,6 +28,63 @@ from oslo_service import eventlet_backdoor
from oslo_service.tests import base
class BackdoorSocketPathTest(base.ServiceBaseTestCase):
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path(self, listen_mock, spawn_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
listen_mock.side_effect = mock.MagicMock()
path = eventlet_backdoor.initialize_if_enabled(self.conf)
self.assertEqual(path, "/tmp/my_special_socket")
@mock.patch.object(os, 'unlink')
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_already_exists(self, listen_mock,
spawn_mock, unlink_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
sock = mock.MagicMock()
listen_mock.side_effect = [socket.error(errno.EADDRINUSE, ''), sock]
path = eventlet_backdoor.initialize_if_enabled(self.conf)
self.assertEqual(path, "/tmp/my_special_socket")
unlink_mock.assert_called_with("/tmp/my_special_socket")
@mock.patch.object(os, 'unlink')
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_already_exists_and_gone(self, listen_mock,
spawn_mock, unlink_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
sock = mock.MagicMock()
listen_mock.side_effect = [socket.error(errno.EADDRINUSE, ''), sock]
unlink_mock.side_effect = OSError(errno.ENOENT, '')
path = eventlet_backdoor.initialize_if_enabled(self.conf)
self.assertEqual(path, "/tmp/my_special_socket")
unlink_mock.assert_called_with("/tmp/my_special_socket")
@mock.patch.object(os, 'unlink')
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_already_exists_and_not_gone(self, listen_mock,
spawn_mock,
unlink_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
listen_mock.side_effect = socket.error(errno.EADDRINUSE, '')
unlink_mock.side_effect = OSError(errno.EPERM, '')
self.assertRaises(OSError, eventlet_backdoor.initialize_if_enabled,
self.conf)
@mock.patch.object(eventlet, 'spawn')
@mock.patch.object(eventlet, 'listen')
def test_backdoor_path_no_perms(self, listen_mock, spawn_mock):
self.config(backdoor_socket="/tmp/my_special_socket")
listen_mock.side_effect = socket.error(errno.EPERM, '')
self.assertRaises(socket.error,
eventlet_backdoor.initialize_if_enabled,
self.conf)
class BackdoorPortTest(base.ServiceBaseTestCase):
@mock.patch.object(eventlet, 'spawn')