From 33dbde59d1628650776db6f96eb7deca28a1b2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 3 Apr 2025 00:53:44 +0200 Subject: [PATCH 1/2] tests: add simple end-to-end test of remote qrexec calls Test vm1 -> relay -> vm2-remote call path. Do it on a single host by having relay service that transform names (source to source-remote and target-remote to target). For this to work, there need to be matching pairs of VMs with and without -remote suffix, and appropriate policy for both outgoing and incoming connections. For now put the test into 'misc' package, when there will be more qubes-air related tests, move it elsewhere. QubesOS/qubes-issues#9015 --- qubes/tests/integ/misc.py | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/qubes/tests/integ/misc.py b/qubes/tests/integ/misc.py index f8c1c110c..d28947b5c 100644 --- a/qubes/tests/integ/misc.py +++ b/qubes/tests/integ/misc.py @@ -192,6 +192,87 @@ def test_121_start_standalone_with_cdrom_vm(self): self.assertFalse(self.vm.is_running()) +class TC_10_RemoteVMMixin: + def setUp(self): + super().setUp() + relay_name = self.make_vm_name("relay") + self.relay_vm = self.app.add_new_vm( + "AppVM", label="green", name=relay_name + ) + self.loop.run_until_complete(self.relay_vm.create_on_disk()) + self.loop.run_until_complete(self.relay_vm.start()) + + self.create_remote_file( + self.relay_vm, + "/etc/qubes-rpc/test.Relay", + """#!/bin/sh + +arg="$1" +target="${arg%%+*}" +target="${target%-remote}" +service="${arg#*+}" + +exec qrexec-client-vm \\ + --source-qube="$QREXEC_REMOTE_DOMAIN-remote" -- \\ + "$target" "$service" +""", + ) + + def test_000_full_connect(self): + """vm1 -> relay -> vm2""" + # This system plays roles of both local and remove systems each VM is + # duplicated - once as normal AppVM and then as RemoteVM, and the + # relay service translates the names + + vm1_name = self.make_vm_name("vm1") + self.vm1 = self.app.add_new_vm("AppVM", label="red", name=vm1_name) + self.loop.run_until_complete(self.vm1.create_on_disk()) + self.vm1_remote = self.app.add_new_vm( + "RemoteVM", + label="red", + name=vm1_name + "-remote", + relayvm=self.relay_vm, + ) + self.vm1_remote.transport_rpc = "test.Relay" + + vm2_name = self.make_vm_name("vm2") + self.vm2 = self.app.add_new_vm("AppVM", label="red", name=vm2_name) + self.loop.run_until_complete(self.vm2.create_on_disk()) + self.vm2_remote = self.app.add_new_vm( + "RemoteVM", + label="red", + name=vm2_name + "-remote", + relayvm=self.relay_vm, + ) + self.vm2_remote.transport_rpc = "test.Relay" + + self.loop.run_until_complete(self.vm1.start()) + file_content = "this is test" + self.create_remote_file( + self.vm1, "/home/user/test-file.txt", file_content + ) + + # first policy allows the call "locally" and the second allows it + # "remotely" + with self.qrexec_policy( + "qubes.Filecopy", vm1_name, vm2_name + "-remote" + ), self.qrexec_policy("qubes.Filecopy", vm1_name + "-remote", vm2_name): + self.loop.run_until_complete( + self.vm1.run_for_stdio( + f"timeout {self.vm2.qrexec_timeout} qvm-copy-to-vm" + f" {vm2_name}-remote /home/user/test-file.txt" + ) + ) + + # check if that worked + received_content, _ = self.loop.run_until_complete( + self.vm2.run_for_stdio( + f"cat /home/user/QubesIncoming/{vm1_name}-remote/test-file.txt" + ) + ) + self.assertEqual(file_content, received_content.decode()) + + def create_testcases_for_templates(): yield from qubes.tests.create_testcases_for_templates( "TC_06_AppVM", @@ -199,6 +280,12 @@ def create_testcases_for_templates(): qubes.tests.SystemTestCase, module=sys.modules[__name__], ) + yield from qubes.tests.create_testcases_for_templates( + "TC_10_RemoteVM", + TC_10_RemoteVMMixin, + qubes.tests.SystemTestCase, + module=sys.modules[__name__], + ) def load_tests(loader, tests, pattern): From e90249b7ce51e0bbce1c9c39fc80230b4cbb856b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 3 Apr 2025 01:38:25 +0200 Subject: [PATCH 2/2] tests: fix cleanup code to handle RemoteVM too RemoteVM has much less attributes, so use getattr to handle this gracefully. At the same time, add also "relayvm" attribute too. QubesOS/qubes-issues#9015 --- qubes/tests/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index d0d1dc35d..c6293a790 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -1146,16 +1146,16 @@ def remove_vms(self, vms): pass # break dependencies for vm in vms: - if vm.default_dispvm in vms: - vm.default_dispvm = None - if vm.netvm in vms: - vm.netvm = None - if vm.management_dispvm in vms: - vm.management_dispvm = None - if vm.guivm in vms: - vm.guivm = None - if vm.audiovm in vms: - vm.audiovm = None + for attr in ( + "default_dispvm", + "netvm", + "management_dispvm", + "guivm", + "audiovm", + "relayvm", + ): + if getattr(vm, attr, None) in vms: + setattr(vm, attr, None) # take app instance from any VM to be removed app = vms[0].app if app.default_dispvm in vms: