Bläddra i källkod

qubes/tools/qubesd: initial version

Wojtek Porczyk 7 år sedan
förälder
incheckning
0be3b1fbb1
6 ändrade filer med 468 tillägg och 1 borttagningar
  1. 1 0
      linux/systemd/Makefile
  2. 10 0
      linux/systemd/qubesd.service
  3. 196 0
      qubes/libvirtaio.py
  4. 243 0
      qubes/tools/qubesd.py
  5. 13 0
      qubes/utils.py
  6. 5 1
      rpm_spec/core-dom0.spec

+ 1 - 0
linux/systemd/Makefile

@@ -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)

+ 10 - 0
linux/systemd/qubesd.service

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

+ 196 - 0
qubes/libvirtaio.py

@@ -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 - 0
qubes/tools/qubesd.py

@@ -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()

+ 13 - 0
qubes/utils.py

@@ -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()

+ 5 - 1
rpm_spec/core-dom0.spec

@@ -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