qubes/tools/qubesd: initial version

This commit is contained in:
Wojtek Porczyk 2017-02-02 13:03:08 +01:00
parent 1be75d9c83
commit 0be3b1fbb1
6 changed files with 468 additions and 1 deletions

View File

@ -12,3 +12,4 @@ install:
cp qubes-reload-firewall@.service $(DESTDIR)$(UNITDIR)
cp qubes-reload-firewall@.timer $(DESTDIR)$(UNITDIR)
cp qubes-qmemman.service $(DESTDIR)$(UNITDIR)
cp qubesd.service $(DESTDIR)$(UNITDIR)

View File

@ -0,0 +1,10 @@
[Unit]
Description=Qubes OS daemon
[Service]
Type=notify
ExecStart=/usr/bin/qubesd
StandardOutput=syslog
[Install]
WantedBy=multi-user.target

196
qubes/libvirtaio.py Normal file
View File

@ -0,0 +1,196 @@
#!/usr/bin/env python3
import asyncio
import collections
import ctypes
import functools
import itertools
import logging
import signal
import libvirt
import pdb
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (
ctypes.py_object, ctypes.c_char_p)
virFreeCallback = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
try:
asyncio.ensure_future
except AttributeError:
asyncio.ensure_future = asyncio.async
class LibvirtAsyncIOEventImpl(object):
class Callback(object):
_iden_counter = itertools.count()
def __init__(self, impl, cb, opaque, *args, **kwargs):
super().__init__(*args, **kwargs)
self.iden = next(self._iden_counter)
self.impl = impl
self.cb = cb
self.opaque = opaque
assert self.iden not in self.impl.callbacks, \
'found {} callback: {!r}'.format(
self.iden, self.impl.callbacks[self.iden])
self.impl.callbacks[self.iden] = self
def close(self):
self.impl.log.debug('callback %d close(), scheduling ff', self.iden)
# Now this is cheating but we have no better option.
caps_cb, caps_opaque, caps_ff = self.opaque
ff = virFreeCallback(ctypes.pythonapi.PyCapsule_GetPointer(
caps_ff, b'virFreeCallback'))
if ff:
real_opaque = ctypes.pythonapi.PyCapsule_GetPointer(
caps_opaque, b'void*')
self.impl.loop.call_soon(ff, real_opaque)
class FDCallback(Callback):
def __init__(self, *args, descriptor, event, **kwargs):
super().__init__(*args, **kwargs)
self.descriptor = descriptor
self.event = event
self.descriptor.callbacks[self.iden] = self
def close(self):
del self.descriptor.callbacks[self.iden]
super().close()
class TimeoutCallback(Callback):
def __init__(self, *args, timeout=None, **kwargs):
super().__init__(*args, **kwargs)
self.timeout = timeout
self.task = None
@asyncio.coroutine
def timer(self):
while True:
try:
t = self.timeout * 1e-3
self.impl.log.debug('sleeping %r', t)
yield from asyncio.sleep(t)
except asyncio.CancelledError:
self.impl.log.debug('timer %d cancelled', self.iden)
break
self.cb(self.iden, self.opaque)
self.impl.log.debug('timer %r callback ended', self.iden)
def start(self):
self.impl.log.debug('timer %r start', self.iden)
if self.task is not None:
return
self.task = asyncio.ensure_future(self.timer())
def stop(self):
self.impl.log.debug('timer %r stop', self.iden)
if self.task is None:
return
self.task.cancel()
self.task = None
def close(self):
self.stop()
super().close()
class DescriptorDict(dict):
class Descriptor(object):
def __init__(self, loop, fd):
self.loop = loop
self.fd = fd
self.callbacks = {}
self.loop.add_reader(
self.fd, self.handle, libvirt.VIR_EVENT_HANDLE_READABLE)
self.loop.add_writer(
self.fd, self.handle, libvirt.VIR_EVENT_HANDLE_WRITABLE)
def close(self):
self.loop.remove_reader(self.fd)
self.loop.remove_writer(self.fd)
def handle(self, event):
for callback in self.callbacks.values():
if callback.event is not None and callback.event & event:
callback.cb(
callback.iden, self.fd, event, callback.opaque)
def __init__(self, loop):
super().__init__()
self.loop = loop
def __missing__(self, fd):
descriptor = self.Descriptor(loop, fd)
self[fd] = descriptor
return descriptor
def __init__(self, loop):
self.loop = loop
self.callbacks = {}
self.descriptors = self.DescriptorDict(self.loop)
self.log = logging.getLogger(self.__class__.__name__)
def register(self):
libvirt.virEventRegisterImpl(
self.add_handle, self.update_handle, self.remove_handle,
self.add_timeout, self.update_timeout, self.remove_timeout)
def add_handle(self, fd, event, cb, opaque):
self.log.debug('add_handle(fd=%d, event=%d, cb=%r, opaque=%r)',
fd, event, cb, opaque)
callback = self.FDCallback(self, cb, opaque,
descriptor=self.descriptors[fd], event=event)
return callback.iden
def update_handle(self, watch, event):
self.log.debug('update_handle(watch=%d, event=%d)', watch, event)
self.callbacks[watch].event = event
def remove_handle(self, watch):
self.log.debug('remove_handle(watch=%d)', watch)
callback = self.callbacks.pop(watch)
callback.close()
# libvirt-python.git/libvirt-override.c suggests that the opaque value
# should be returned. This is horribly wrong, because this would cause
# instant execution of ff callback, which is prohibited by libvirt's
# C API documentation. We therefore intentionally return None.
return None
def add_timeout(self, timeout, cb, opaque):
self.log.debug('add_timeout(timeout=%d, cb=%r, opaque=%r)',
timeout, cb, opaque)
if timeout <= 0:
# TODO we could think about registering timeouts of -1 as a special
# case and emulate 0 somehow (60 Hz?)
self.log.warning('will not add timer with timeout %r', timeout)
return -1
callback = self.TimeoutCallback(self, cb, opaque, timeout=timeout)
callback.start()
return callback.iden
def update_timeout(self, timer, timeout):
self.log.debug('update_timeout(timer=%d, timeout=%d)', timer, timeout)
callback = self.callbacks[timer]
callback.timeout = timeout
if timeout > 0:
callback.start()
else:
callback.stop()
def remove_timeout(self, timer):
self.log.debug('remove_timeout(timer=%d)', timer)
callback = self.callbacks.pop(timer)
callback.close()
# See remove_handle()
return None

243
qubes/tools/qubesd.py Normal file
View File

@ -0,0 +1,243 @@
#!/usr/bin/env python3.6
import asyncio
import functools
import io
import json
import operator
import os
import reprlib
import signal
import socket
import sys
import types
import qubes
import qubes.libvirtaio
import qubes.utils
import qubes.vm.qubesvm
QUBESD_SOCK = '/var/run/qubesd.sock'
class ProtocolRepr(reprlib.Repr):
def repr1(self, x, level):
if isinstance(x, qubes.vm.qubesvm.QubesVM):
x = x.name
return super().repr1(x, level)
def repr_str(self, x, level):
'''Warning: this is incompatible with python 3 wrt to b'' '''
return "'{}'".format(''.join(
chr(c)
if 0x20 < c < 0x7f and c not in (ord("'"), ord('\\'))
else '\\x{:02x}'.format(c)
for c in x.encode()))
def repr_Label(self, x, level):
return self.repr1(x.name, level)
class ProtocolError(AssertionError):
'''Raised when something is wrong with data received'''
pass
class PermissionDenied(Exception):
'''Raised deliberately by handlers when we decide not to cooperate'''
pass
def not_in_api(func):
func.not_in_api = True
return func
class QubesMgmt(object):
def __init__(self, app, src, method, dest, arg):
self.app = app
self.src = self.app.domains[src.decode('ascii')]
self.dest = self.app.domains[dest.decode('ascii')]
self.arg = arg.decode('ascii')
self.prepr = ProtocolRepr()
self.method = method.decode('ascii')
untrusted_func_name = self.method
if untrusted_func_name.startswith('mgmt.'):
untrusted_func_name = untrusted_func_name[5:]
untrusted_func_name = untrusted_func_name.lower().replace('.', '_')
if untrusted_func_name.startswith('_') \
or not '_' in untrusted_func_name:
raise ProtocolError(
'possibly malicious function name: {!r}'.format(
untrusted_func_name))
try:
untrusted_func = getattr(self, untrusted_func_name)
except AttributeError:
raise ProtocolError(
'no such attribute: {!r}'.format(
untrusted_function_name))
if not isinstance(untrusted_func, types.MethodType):
raise ProtocolError(
'no such method: {!r}'.format(
untrusted_function_name))
if getattr(untrusted_func, 'not_in_api', False):
raise ProtocolError(
'attempt to call private method: {!r}'.format(
untrusted_function_name))
self.execute = untrusted_func
del untrusted_func_name
del untrusted_func
#
# PRIVATE METHODS, not to be called via RPC
#
@not_in_api
def fire_event_for_permission(self, *args, **kwargs):
return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
self.dest, self.arg, *args, **kwargs)
@not_in_api
def repr(self, *args, **kwargs):
return self.prepr.repr(*args, **kwargs)
#
# ACTUAL RPC CALLS
#
def vm_list(self, untrusted_payload):
assert self.dest.name == 'dom0'
assert not self.arg
assert not untrusted_payload
del untrusted_payload
domains = self.app.domains
for selector in self.fire_event_for_permission():
domains = filter(selector, domains)
return ''.join('{} class={} state={}\n'.format(
self.repr(vm),
vm.__class__.__name__,
vm.get_power_state())
for vm in sorted(domains))
def vm_property_get(self, untrusted_payload):
assert self.arg in self.dest.property_list()
assert not untrusted_payload
del untrusted_payload
self.fire_event_for_permission()
try:
value = getattr(self.dest, self.arg)
except AttributeError:
return 'default=True '
else:
return 'default={} {}'.format(
str(dest.property_is_default(self.arg)),
self.repr(self.value))
class QubesDaemonProtocol(asyncio.Protocol):
buffer_size = 65536
def __init__(self, *args, app, **kwargs):
super().__init__()
self.app = app
self.untrusted_buffer = io.BytesIO()
self.untrusted_buffer_trusted_len = 0
def connection_made(self, transport):
print('connection_made()')
self.transport = transport
def connection_lost(self, exc):
print('connection_lost(exc={!r})'.format(exc))
self.untrusted_buffer.close()
def data_received(self, untrusted_data):
print('data_received(untrusted_data={!r})'.format(untrusted_data))
if self.untrusted_buffer_trusted_len + len(untrusted_data) \
> self.buffer_size:
print(' request too long')
self.transport.close()
return
self.untrusted_buffer_trusted_len += \
self.untrusted_buffer.write(untrusted_data)
def eof_received(self):
print('eof_received()')
try:
src, method, dest, arg, untrusted_payload = \
self.untrusted_buffer.getvalue().split(b'\0', 4)
except ValueError:
# TODO logging
return
try:
mgmt = QubesMgmt(self.app, src, method, dest, arg)
response = mgmt.execute(untrusted_payload=untrusted_payload)
except PermissionDenied as err:
# TODO logging
return
except ProtocolError as err:
# TODO logging
print(repr(err))
return
except AssertionError:
# TODO logging
print(repr(err))
return
self.transport.write(response.encode('ascii'))
try:
self.transport.write_eof()
except NotImplementedError:
pass
def sighandler(loop, signame, server):
print('caught {}, exiting'.format(signame))
server.close()
loop.stop()
parser = qubes.tools.QubesArgumentParser(description='Qubes OS daemon')
def main(args=None):
args = parser.parse_args(args)
loop = asyncio.get_event_loop()
qubes.libvirtaio.LibvirtAsyncIOEventImpl(loop).register()
try:
os.unlink(QUBESD_SOCK)
except FileNotFoundError:
pass
old_umask = os.umask(0o007)
server = loop.run_until_complete(loop.create_unix_server(
functools.partial(QubesDaemonProtocol, app=args.app), QUBESD_SOCK))
os.umask(old_umask)
del old_umask
for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame),
sighandler, loop, signame, server)
qubes.utils.systemd_notify()
try:
loop.run_forever()
loop.run_until_complete(server.wait_closed())
finally:
loop.close()
if __name__ == '__main__':
main()

View File

@ -26,6 +26,7 @@ import random
import string
import os
import re
import socket
import subprocess
import pkg_resources
@ -163,3 +164,15 @@ def random_string(length=5):
''' Return random string consisting of ascii_leters and digits '''
return ''.join(random.choice(string.ascii_letters + string.digits)
for _ in range(length))
def systemd_notify():
'''Notify systemd'''
nofity_socket = os.getenv('NOTIFY_SOCKET')
if not nofity_socket:
return
if nofity_socket.startswith('@'):
nofity_socket = '\0' + nofity_socket[1:]
s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
s.connect(nofity_socket)
s.sendall(b'READY=1')
s.close()

View File

@ -208,6 +208,7 @@ fi
/usr/bin/qvm-*
/usr/bin/qubes-*
/usr/bin/qmemmand
/usr/bin/qubesd
%dir %{python3_sitelib}/qubes-*.egg-info
%{python3_sitelib}/qubes-*.egg-info/*
@ -223,8 +224,9 @@ fi
%{python3_sitelib}/qubes/devices.py
%{python3_sitelib}/qubes/dochelpers.py
%{python3_sitelib}/qubes/events.py
%{python3_sitelib}/qubes/firewall.py
%{python3_sitelib}/qubes/exc.py
%{python3_sitelib}/qubes/firewall.py
%{python3_sitelib}/qubes/libvirtaio.py
%{python3_sitelib}/qubes/log.py
%{python3_sitelib}/qubes/rngdoc.py
%{python3_sitelib}/qubes/tarwriter.py
@ -264,6 +266,7 @@ fi
%{python3_sitelib}/qubes/tools/qubes_create.py
%{python3_sitelib}/qubes/tools/qubes_monitor_layout_notify.py
%{python3_sitelib}/qubes/tools/qubes_prefs.py
%{python3_sitelib}/qubes/tools/qubesd.py
%{python3_sitelib}/qubes/tools/qvm_block.py
%{python3_sitelib}/qubes/tools/qvm_backup.py
%{python3_sitelib}/qubes/tools/qvm_backup_restore.py
@ -386,6 +389,7 @@ fi
%{_unitdir}/qubes-netvm.service
%{_unitdir}/qubes-qmemman.service
%{_unitdir}/qubes-vm@.service
%{_unitdir}/qubesd.service
%{_unitdir}/qubes-reload-firewall@.service
%{_unitdir}/qubes-reload-firewall@.timer
%attr(2770,root,qubes) %dir /var/lib/qubes