diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index db69003e3..e2d084ba7 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -1404,10 +1404,10 @@ def enter_keys_in_window(self, title, keys): ] + keys subprocess.check_call(command) - def shutdown_and_wait(self, vm, timeout=60): + def shutdown_and_wait(self, vm, force=False, timeout=60): try: self.loop.run_until_complete( - vm.shutdown(wait=True, timeout=timeout) + vm.shutdown(wait=True, force=force, timeout=timeout) ) except qubes.exc.QubesException: name = vm.name diff --git a/qubes/tests/integ/network.py b/qubes/tests/integ/network.py index d114e9e11..eb26dc301 100644 --- a/qubes/tests/integ/network.py +++ b/qubes/tests/integ/network.py @@ -165,9 +165,11 @@ def tearDown(self): del self.netvms super(VmNetworkingMixin, self).tearDown() - def configure_netvm(self): + def configure_netvm(self, netvms: list = None): """ :type self: qubes.tests.SystemTestCase | VmNetworkingMixin + + :param list netvms: Use specified netvms or self.netvms. """ def run_netvm_cmd(qube, cmd): @@ -181,7 +183,9 @@ def run_netvm_cmd(qube, cmd): % (qube, cmd, e.stdout.decode(), e.stderr.decode()) ) - for qube in self.netvms: + if not netvms: + netvms = self.netvms + for qube in netvms: if not qube.is_running(): self.loop.run_until_complete(self.start_vm(qube)) # Ensure that dnsmasq is installed: @@ -254,8 +258,8 @@ def _networking_paused_from_none_to_existent( 0, "Ping by name on " + test + " failed", ) - self.assertEqual( - self.testvm1.features.get("deferred-netvm-original", None), None + self.assertIsNone( + self.testvm1.features.get("deferred-netvm-original", None) ) self.shutdown_and_wait(self.testvm1) @@ -284,8 +288,8 @@ def _networking_paused_from_existent_to_none( 0, "Ping by name on " + test + " succeeded but should have failed", ) - self.assertEqual( - self.testvm1.features.get("deferred-netvm-original", None), None + self.assertIsNone( + self.testvm1.features.get("deferred-netvm-original", None) ) self.shutdown_and_wait(self.testvm1) @@ -317,8 +321,8 @@ def _networking_paused_change_shutdown_old( 0, "Ping by name on " + test + " failed", ) - self.assertEqual( - self.testvm1.features.get("deferred-netvm-original", None), None + self.assertIsNone( + self.testvm1.features.get("deferred-netvm-original", None) ) self.shutdown_and_wait(self.testvm1) @@ -355,8 +359,75 @@ def _networking_paused_change_purge_old( 0, "Ping by name on " + test + " failed", ) + self.assertIsNone( + self.testvm1.features.get("deferred-netvm-original", None) + ) + self.shutdown_and_wait(self.testvm1) + + def _networking_paused_restart_netvm( + self, ip, name, ip_deadline, name_deadline + ): + test = "restart netvm and unpause client" + self.log.critical(test) + print(test) + self.testvm1.netvm = self.testnetvm + self.loop.run_until_complete(self.start_vm(self.testvm1)) + self.loop.run_until_complete(self.testvm1.pause()) + self.shutdown_and_wait(self.testnetvm, force=True) + self.assertEqual( + self.testvm1.features.get("deferred-netvm-original", None), + self.testnetvm.name, + ) + self.loop.run_until_complete(self.start_vm(self.testnetvm)) + self.configure_netvm([self.testnetvm]) + self.loop.run_until_complete(self.testvm1.unpause()) + self._run_cmd_and_log_output(self.testvm1, ip_deadline) + self._run_cmd_and_log_output(self.testvm1, name_deadline) + self.assertEqual( + self.run_cmd(self.testvm1, ip), + 0, + "Ping by IP on " + test + " failed", + ) + self.assertEqual( + self.run_cmd(self.testvm1, name), + 0, + "Ping by name on " + test + " failed", + ) + self.assertIsNone( + self.testvm1.features.get("deferred-netvm-original", None) + ) + self.shutdown_and_wait(self.testvm1) + + def _networking_paused_shutdown_netvm( + self, ip, name, ip_deadline, name_deadline + ): + test = "shutdown netvm and unpause client" + self.log.critical(test) + print(test) + self.testvm1.netvm = self.testnetvm + self.loop.run_until_complete(self.start_vm(self.testvm1)) + self.loop.run_until_complete(self.testvm1.pause()) + self.shutdown_and_wait(self.testnetvm, force=True) + self.assertEqual( + self.testvm1.features.get("deferred-netvm-original", None), + self.testnetvm.name, + ) + self.loop.run_until_complete(self.testvm1.unpause()) + self.configure_netvm([self.testnetvm]) + self._run_cmd_and_log_output(self.testvm1, ip_deadline) + self._run_cmd_and_log_output(self.testvm1, name_deadline) self.assertEqual( - self.testvm1.features.get("deferred-netvm-original", None), None + self.run_cmd(self.testvm1, ip), + 0, + "Ping by IP on " + test + " failed", + ) + self.assertEqual( + self.run_cmd(self.testvm1, name), + 0, + "Ping by name on " + test + " failed", + ) + self.assertIsNone( + self.testvm1.features.get("deferred-netvm-original", None) ) self.shutdown_and_wait(self.testvm1) @@ -406,6 +477,28 @@ def test_001_simple_networking_paused_change_purge_old(self): self.ping_deadline_name, ) + def test_001_simple_networking_paused_restart_netvm(self): + """ + :type self: qubes.tests.SystemTestCase | VmNetworkingMixin + """ + self._networking_paused_restart_netvm( + self.ping_ip, + self.ping_name, + self.ping_deadline_ip, + self.ping_deadline_name, + ) + + def test_001_simple_networking_paused_shutdown_netvm(self): + """ + :type self: qubes.tests.SystemTestCase | VmNetworkingMixin + """ + self._networking_paused_shutdown_netvm( + self.ping_ip, + self.ping_name, + self.ping_deadline_ip, + self.ping_deadline_name, + ) + def test_010_simple_proxyvm(self): """ :type self: qubes.tests.SystemTestCase | VmNetworkingMixin diff --git a/qubes/tests/integ/network_ipv6.py b/qubes/tests/integ/network_ipv6.py index 646638acb..ed13805d3 100644 --- a/qubes/tests/integ/network_ipv6.py +++ b/qubes/tests/integ/network_ipv6.py @@ -74,9 +74,11 @@ def tearDown(self): ) super().tearDown() - def configure_netvm(self): + def configure_netvm(self, netvms: list = None): """ :type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin + + :param list netvms: Use specified netvms or self.netvms. """ def run_netvm_cmd(qube, cmd): @@ -90,11 +92,13 @@ def run_netvm_cmd(qube, cmd): % (cmd, e.stdout.decode(), e.stderr.decode()) ) - for qube in self.netvms: + if not netvms: + netvms = self.netvms + for qube in netvms: qube.features["ipv6"] = True - super(VmIPv6NetworkingMixin, self).configure_netvm() + super(VmIPv6NetworkingMixin, self).configure_netvm(netvms) - for qube in self.netvms: + for qube in netvms: run_netvm_cmd( qube, "ip addr add {}/128 dev test0".format(self.test_ip6) ) @@ -167,6 +171,28 @@ def test_501_simple_networking_paused_change_purge_old(self): self.ping6_deadline_name, ) + def test_501_simple_networking_paused_restart_netvm(self): + """ + :type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin + """ + self._networking_paused_restart_netvm( + self.ping6_ip, + self.ping6_name, + self.ping6_deadline_ip, + self.ping6_deadline_name, + ) + + def test_501_simple_networking_paused_shutdown_netvm(self): + """ + :type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin + """ + self._networking_paused_shutdown_netvm( + self.ping6_ip, + self.ping6_name, + self.ping6_deadline_ip, + self.ping6_deadline_name, + ) + def test_510_ipv6_simple_proxyvm(self): """ :type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin diff --git a/qubes/tests/vm/mix/net.py b/qubes/tests/vm/mix/net.py index 1d286262f..30ce2134d 100644 --- a/qubes/tests/vm/mix/net.py +++ b/qubes/tests/vm/mix/net.py @@ -178,31 +178,38 @@ def test_146_netvm_defer(self): self.loop.run_until_complete(vm.apply_deferred_netvm()) mock_detach.assert_not_called() mock_attach.assert_not_called() - mock_detach.reset_mock() mock_attach.reset_mock() + with self.subTest("changing netvm and restoring original netvm"): original_netvm = vm.netvm.name - vm.netvm = self.netvm2 + vm.netvm = vm.netvm mock_detach.assert_not_called() mock_attach.assert_not_called() self.assertEqual( vm.features.get("deferred-netvm-original", None), original_netvm, ) - vm.netvm = original_netvm + with patch( + "qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False + ): + self.loop.run_until_complete(vm.apply_deferred_netvm()) self.assertEqual( vm.features.get("deferred-netvm-original", None), None ) - mock_detach.assert_not_called() - mock_attach.assert_not_called() - + mock_detach.assert_called() + mock_attach.assert_called() + with patch( + "qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False + ): + vm.netvm = vm.netvm mock_detach.reset_mock() mock_attach.reset_mock() + + original_netvm = vm.netvm.name with self.subTest( "changing netvm and restoring original netvm from none" ): - original_netvm = vm.netvm.name with patch( "qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False ): @@ -218,17 +225,25 @@ def test_146_netvm_defer(self): ) vm.netvm = None self.assertEqual( - vm.features.get("deferred-netvm-original", None), None + vm.features.get("deferred-netvm-original", None), "" ) mock_detach.assert_not_called() mock_attach.assert_not_called() with patch( "qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False ): - vm.netvm = original_netvm + self.loop.run_until_complete(vm.apply_deferred_netvm()) + self.assertEqual( + vm.features.get("deferred-netvm-original", None), None + ) + mock_detach.assert_called() + mock_attach.assert_not_called() + with patch("qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False): + vm.netvm = original_netvm mock_detach.reset_mock() mock_attach.reset_mock() + with self.subTest("changing netvm"): original_netvm = vm.netvm.name vm.netvm = self.netvm2 diff --git a/qubes/vm/dispvm.py b/qubes/vm/dispvm.py index dca744a87..a9cb7f99a 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -749,18 +749,18 @@ async def use_preload(self) -> None: self.log.info("Using preloaded qube") if not appvm.features.get("internal", None): del self.features["internal"] - await self.apply_deferred_netvm() self.preload_requested = False + await self.apply_deferred_netvm() del self.features["preload-dispvm-in-progress"] else: # Happens when unpause/resume occurs without qube being requested. self.log.warning("Using a preloaded qube before requesting it") if not appvm.features.get("internal", None): del self.features["internal"] - await self.apply_deferred_netvm() appvm.remove_preload_from_list( [self.name], reason="qube was used without being requested" ) + await self.apply_deferred_netvm() self.features["preload-dispvm-in-progress"] = False self.app.save() asyncio.ensure_future( diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index fa2f2426b..89b68ef32 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -315,6 +315,9 @@ def on_domain_shutdown_net(self, event, **kwargs): This will allow re-reconnecting them cleanly later. """ # pylint: disable=unused-argument + deferred_from = self.features.get("deferred-netvm-original", None) + if deferred_from is not None: + del self.features["deferred-netvm-original"] for vm in self.connected_vms: if not vm.is_running(): continue @@ -325,7 +328,7 @@ def on_domain_shutdown_net(self, event, **kwargs): pass @qubes.events.handler("domain-start") - def on_domain_started_net(self, event, **kwargs): + async def on_domain_started_net(self, event, **kwargs): """Connect this domain to its downstream domains. Also reload firewall in its netvm. @@ -336,11 +339,15 @@ def on_domain_started_net(self, event, **kwargs): self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member for vm in self.connected_vms: - if not vm.is_running(): + if not vm.is_running() or vm.is_paused(): continue vm.log.info("Attaching network") try: - vm.attach_network() + deferred_from = vm.features.get("deferred-netvm-original", None) + if deferred_from is not None: + await vm.apply_deferred_netvm() + else: + vm.attach_network() except (qubes.exc.QubesException, libvirt.libvirtError): vm.log.warning("Cannot attach network", exc_info=1) @@ -358,6 +365,11 @@ async def apply_deferred_netvm(self): oldvalue = None if deferred_from in self.app.domains: oldvalue = self.app.domains[deferred_from] + if self.netvm and not self.netvm.is_running(): + # Too early to create routing table. Happens when shutting down + # netvm and not starting it before attempting to use the client. + # This function will be called again by domain-start of the netvm. + return self.fire_event( "property-pre-set:netvm", pre_event=True, @@ -442,22 +454,32 @@ def detach_network(self): if not self.is_running(): raise qubes.exc.QubesVMNotRunningError(self) + deferred_from = self.features.get("deferred-netvm", None) if self.netvm is None: - deferred_from = self.features.get("deferred-netvm", None) if deferred_from is not None: raise qubes.exc.QubesVMError( self, "netvm should not be {}".format(self.netvm) ) + # Properties extracted from libvirt_domain to support deferred netvm. + root = lxml.etree.fromstring(self.libvirt_domain.XMLDesc()) + eth = root.find(".//interface[@type='ethernet']") + if self.is_paused(): self.log.warning( "Deferred detaching libvirt net device because qube is paused" ) + if deferred_from is not None or eth is None: + return + backend = eth.find("backenddomain") + if backend is None: + return + backend_name = backend.get("name") + if backend_name not in self.app.domains: + return + self.features["deferred-netvm-original"] = str(backend_name) return - # Properties extracted from libvirt_domain to support deferred netvm. - root = lxml.etree.fromstring(self.libvirt_domain.XMLDesc()) - eth = root.find(".//interface[@type='ethernet']") if eth is None: return self.libvirt_domain.detachDevice(lxml.etree.tostring(eth).decode()) @@ -587,14 +609,6 @@ def on_property_pre_set_netvm(self, event, name, newvalue, oldvalue=None): ) deferred_from = self.features.get("deferred-netvm-original", None) - if deferred_from is not None and ( - (not deferred_from and not newvalue) - or (newvalue and deferred_from == newvalue.name) - ): - # No deferred netvm as original value is restored. - del self.features["deferred-netvm-original"] - return - if self.is_paused(): if deferred_from is None: self.features["deferred-netvm-original"] = getattr(