diff --git a/qubes/api/misc.py b/qubes/api/misc.py index 50652e3c..b95e75e3 100644 --- a/qubes/api/misc.py +++ b/qubes/api/misc.py @@ -76,7 +76,8 @@ class QubesMiscAPI(qubes.api.AbstractQubesAPI): untrusted_features = {} safe_set = string.ascii_letters + string.digits - expected_features = ('qrexec', 'gui', 'default-user') + expected_features = ('qrexec', 'gui', 'gui-emulated', 'default-user', + 'os') for feature in expected_features: untrusted_value = self.src.untrusted_qdb.read( '/qubes-tools/' + feature) diff --git a/qubes/ext/core_features.py b/qubes/ext/core_features.py index b2b77ea2..f07f5e93 100644 --- a/qubes/ext/core_features.py +++ b/qubes/ext/core_features.py @@ -32,7 +32,7 @@ class CoreFeatures(qubes.ext.Extension): return requested_features = {} - for feature in ('qrexec', 'gui', 'qubes-firewall'): + for feature in ('qrexec', 'gui', 'gui-emulated', 'qubes-firewall'): untrusted_value = untrusted_features.get(feature, None) if untrusted_value in ('1', '0'): requested_features[feature] = bool(int(untrusted_value)) @@ -44,7 +44,7 @@ class CoreFeatures(qubes.ext.Extension): # gui agent presence (0 or 1) qrexec_before = vm.features.get('qrexec', False) - for feature in ('qrexec', 'gui'): + for feature in ('qrexec', 'gui', 'gui-emulated'): # do not allow (Template)VM to override setting if already set # some other way if feature in requested_features and feature not in vm.features: diff --git a/qubes/ext/windows.py b/qubes/ext/windows.py new file mode 100644 index 00000000..57ddb1e8 --- /dev/null +++ b/qubes/ext/windows.py @@ -0,0 +1,64 @@ +# -*- encoding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2017 Marek Marczykowski-Górecki +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, see . + +import qubes.ext + +class WindowsFeatures(qubes.ext.Extension): + # pylint: disable=too-few-public-methods + @qubes.ext.handler('features-request') + def qubes_features_request(self, vm, event, untrusted_features): + '''Handle features provided requested by Qubes Windows Tools''' + # pylint: disable=no-self-use,unused-argument + if getattr(vm, 'template', None): + vm.log.warning( + 'Ignoring qubes.NotifyTools for template-based VM') + return + + guest_os = None + if 'os' in untrusted_features: + if untrusted_features['os'] in ['Windows']: + guest_os = untrusted_features['os'] + + qrexec = None + if 'qrexec' in untrusted_features: + if untrusted_features['qrexec'] == '1': + # qrexec feature is set by CoreFeatures extension + qrexec = True + + del untrusted_features + + if guest_os: + vm.features['os'] = guest_os + if guest_os == 'Windows' and qrexec: + vm.features['rpc-clipboard'] = True + + @qubes.ext.handler('domain-add', system=True) + def on_domain_add(self, app, _event, vm, **kwargs): + # pylint: disable=no-self-use,unused-argument + if getattr(vm, 'template', None) is None: + # handle only template-based vms + return + + template = vm.template + if template.features.check_with_template('os', None) != 'Windows': + # ignore non-windows templates + return + + # TODO: consider copying template's root volume here diff --git a/qubes/tests/api_misc.py b/qubes/tests/api_misc.py index b7ffc9a0..8eddd3e2 100644 --- a/qubes/tests/api_misc.py +++ b/qubes/tests/api_misc.py @@ -131,11 +131,14 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase): self.assertEqual(self.src.mock_calls, [ mock.call.untrusted_qdb.read('/qubes-tools/qrexec'), mock.call.untrusted_qdb.read('/qubes-tools/gui'), + mock.call.untrusted_qdb.read('/qubes-tools/gui-emulated'), mock.call.untrusted_qdb.read('/qubes-tools/default-user'), + mock.call.untrusted_qdb.read('/qubes-tools/os'), mock.call.fire_event_async('features-request', untrusted_features={ 'gui': '1', 'default-user': 'user', - 'qrexec': '1'}), + 'qrexec': '1', + 'os': 'Linux'}), ('fire_event_async().__iter__', (), {}), ]) self.assertEqual(self.app.mock_calls, [mock.call.save()]) @@ -153,11 +156,14 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase): self.assertEqual(self.src.mock_calls, [ mock.call.untrusted_qdb.read('/qubes-tools/qrexec'), mock.call.untrusted_qdb.read('/qubes-tools/gui'), + mock.call.untrusted_qdb.read('/qubes-tools/gui-emulated'), mock.call.untrusted_qdb.read('/qubes-tools/default-user'), + mock.call.untrusted_qdb.read('/qubes-tools/os'), mock.call.fire_event_async('features-request', untrusted_features={ 'gui': '1', 'default-user': 'user', - 'qrexec': '1'}), + 'qrexec': '1', + 'os': 'Linux'}), ('fire_event_async().__iter__', (), {}), ]) self.assertEqual(self.app.mock_calls, [mock.call.save()]) diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 1197875c..bb937f68 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -21,6 +21,7 @@ from unittest import mock import qubes.ext.core_features +import qubes.ext.windows import qubes.tests @@ -163,3 +164,53 @@ class TC_00_CoreFeatures(qubes.tests.QubesTestCase): ('features.__contains__', ('qrexec',), {}), ('features.__contains__', ('gui',), {}), ]) + +class TC_10_WindowsFeatures(qubes.tests.QubesTestCase): + def setUp(self): + super().setUp() + self.ext = qubes.ext.windows.WindowsFeatures() + self.vm = mock.MagicMock() + self.features = {} + self.vm.configure_mock(**{ + 'features.get.side_effect': self.features.get, + 'features.__contains__.side_effect': self.features.__contains__, + 'features.__setitem__.side_effect': self.features.__setitem__, + }) + + def test_000_notify_tools_full(self): + del self.vm.template + self.ext.qubes_features_request(self.vm, 'features-request', + untrusted_features={ + 'gui': '1', + 'version': '1', + 'default-user': 'user', + 'qrexec': '1', + 'os': 'Windows'}) + self.assertEqual(self.vm.mock_calls, [ + ('features.__setitem__', ('os', 'Windows'), {}), + ('features.__setitem__', ('rpc-clipboard', True), {}), + ]) + + def test_001_notify_tools_no_qrexec(self): + del self.vm.template + self.ext.qubes_features_request(self.vm, 'features-request', + untrusted_features={ + 'gui': '1', + 'version': '1', + 'default-user': 'user', + 'qrexec': '0', + 'os': 'Windows'}) + self.assertEqual(self.vm.mock_calls, [ + ('features.__setitem__', ('os', 'Windows'), {}), + ]) + + def test_002_notify_tools_other_os(self): + del self.vm.template + self.ext.qubes_features_request(self.vm, 'features-request', + untrusted_features={ + 'gui': '1', + 'version': '1', + 'default-user': 'user', + 'qrexec': '1', + 'os': 'Linux'}) + self.assertEqual(self.vm.mock_calls, []) diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index 2e3a5f54..283434cb 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -284,6 +284,7 @@ fi %{python3_sitelib}/qubes/ext/qubesmanager.py %{python3_sitelib}/qubes/ext/r3compatibility.py %{python3_sitelib}/qubes/ext/services.py +%{python3_sitelib}/qubes/ext/windows.py %dir %{python3_sitelib}/qubes/tests %dir %{python3_sitelib}/qubes/tests/__pycache__ diff --git a/setup.py b/setup.py index 1f3ae9ce..d2110ac2 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ if __name__ == '__main__': 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension', 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension', 'qubes.ext.services = qubes.ext.services:ServicesExtension', + 'qubes.ext.windows = qubes.ext.windows:WindowsFeatures', ], 'qubes.devices': [ 'pci = qubes.ext.pci:PCIDevice',