Skip to content

Commit a1a84cc

Browse files
committed
WIP: Introduce RelayVM property for RemoteVM
1 parent 8e4b769 commit a1a84cc

7 files changed

Lines changed: 172 additions & 15 deletions

File tree

qubes/api/internal.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class SystemInfoCache:
4949
"property-reset:icon",
5050
"property-set:guivm",
5151
"property-reset:guivm",
52+
"property-set:relayvm",
53+
"property-reset:relayvm",
54+
"property-set:transport_rpc",
55+
"property-reset:transport_rpc",
5256
# technically not changeable, but keep for consistency
5357
"property-set:uuid",
5458
"property-reset:uuid",
@@ -125,6 +129,16 @@ def get_system_info(cls, app):
125129
if getattr(domain, "guivm", None)
126130
else None
127131
),
132+
"relayvm": (
133+
domain.relayvm.name
134+
if getattr(domain, "relayvm", None)
135+
else None
136+
),
137+
"transport_rpc": (
138+
domain.transport_rpc
139+
if getattr(domain, "transport_rpc", None)
140+
else None
141+
),
128142
"power_state": domain.get_power_state(),
129143
"uuid": str(domain.uuid),
130144
}

qubes/ext/relay.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#
2+
# The Qubes OS Project, https://www.qubes-os.org/
3+
#
4+
# Copyright (C) 2024 Frédéric Pierret <frederic.pierret@qubes-os.org>
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
18+
#
19+
20+
import qubes.ext
21+
import qubes.vm.remotevm
22+
23+
24+
class Relay(qubes.ext.Extension):
25+
# pylint: disable=unused-argument
26+
@qubes.ext.handler("domain-init", "domain-load")
27+
def on_domain_init_load(self, vm, event):
28+
if (
29+
getattr(vm, "relayvm", None)
30+
and "relayvm-" + vm.relayvm.name not in vm.tags
31+
):
32+
self.on_property_set(vm, event, name="relayvm", newvalue=vm.relayvm)
33+
34+
@qubes.ext.handler("domain-start")
35+
def on_domain_start(self, vm, event, **kwargs):
36+
if not vm.untrusted_qdb:
37+
return
38+
for domain in vm.app.domains:
39+
if getattr(domain, "relayvm", None) and domain.relayvm == vm:
40+
vm.untrusted_qdb.write(
41+
f"/remote/{domain.name}", domain.remote_name or domain.name
42+
)
43+
44+
@qubes.ext.handler("property-reset:relayvm", vm=qubes.vm.remotevm.RemoteVM)
45+
def on_property_reset(self, subject, event, name, oldvalue=None):
46+
newvalue = getattr(subject, "relayvm", None)
47+
self.on_property_set(subject, event, name, newvalue, oldvalue)
48+
49+
@qubes.ext.handler("property-set:relayvm", vm=qubes.vm.remotevm.RemoteVM)
50+
def on_property_set(self, subject, event, name, newvalue, oldvalue=None):
51+
# Clean other 'relayvm-XXX' tags.
52+
# qrexec-client-vm can connect to only one domain
53+
tags_list = list(subject.tags)
54+
for tag in tags_list:
55+
if tag.startswith("relayvm-"):
56+
subject.tags.remove(tag)
57+
58+
if newvalue:
59+
relayvm_tag = "relayvm-" + newvalue.name
60+
subject.tags.add(relayvm_tag)
61+
if newvalue.untrusted_qdb:
62+
remote_name = subject.remote_name or subject.name
63+
newvalue.untrusted_qdb.write(
64+
f"/remote/{subject.name}", remote_name
65+
)

qubes/tests/api_internal.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ async def coro_f(*args, **kwargs):
3737

3838

3939
class TC_00_API_Misc(qubes.tests.QubesTestCase):
40+
maxDiff = None
41+
4042
def setUp(self):
4143
super().setUp()
4244
self.app = mock.NonCallableMock()
@@ -195,6 +197,8 @@ def test_010_get_system_info(self):
195197
"icon": "icon-dom0",
196198
"guivm": None,
197199
"power_state": "Running",
200+
"relayvm": None,
201+
"transport_rpc": None,
198202
"uuid": "00000000-0000-0000-0000-000000000000",
199203
},
200204
"vm": {
@@ -205,6 +209,8 @@ def test_010_get_system_info(self):
205209
"icon": "icon-vm",
206210
"guivm": "vm",
207211
"power_state": "Halted",
212+
"relayvm": None,
213+
"transport_rpc": None,
208214
"uuid": str(TEST_UUID),
209215
},
210216
}

qubes/tests/app.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#
2222

2323
import os
24+
import unittest
2425
import unittest.mock as mock
2526

2627
import lxml.etree
@@ -35,6 +36,8 @@
3536
import logging
3637
import time
3738

39+
from qubes.tests.vm.qubesvm import TestQubesDB
40+
3841

3942
class TestApp(qubes.tests.TestEmitter):
4043
pass
@@ -919,10 +922,8 @@ def test_116_remotevm_add_and_remove(self):
919922
remotevm1 = self.app.add_new_vm(
920923
"RemoteVM", name="remote-vm1", label="blue"
921924
)
922-
remotevm2 = self.app.add_new_vm(
923-
"RemoteVM", name="remote-vm2", label="gray"
924-
)
925-
qubesvm1 = self.app.add_new_vm(
925+
self.app.add_new_vm("RemoteVM", name="remote-vm2", label="gray")
926+
self.app.add_new_vm(
926927
"AppVM",
927928
name="test-vm",
928929
template=self.template,
@@ -947,6 +948,57 @@ def test_117_remotevm_status(self):
947948
remotevm1.get_mem(),
948949
] == ["Running", 0, 0]
949950

951+
@unittest.mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb")
952+
def test_118_remotevm_set_relayvm(self, mock_qubesdb):
953+
class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
954+
relayvm = qubes.property("relayvm")
955+
transport_rpc = qubes.property("transport_rpc")
956+
957+
localrelay = self.app.add_new_vm(
958+
"AppVM",
959+
name="local-relay",
960+
template=self.template,
961+
label="red",
962+
)
963+
# add QDB to localrelay
964+
test_qubesdb = TestQubesDB()
965+
mock_qubesdb.write.side_effect = test_qubesdb.write
966+
mock_qubesdb.rm.side_effect = test_qubesdb.rm
967+
localrelay.untrusted_qdb = test_qubesdb
968+
969+
remotevm = self.app.add_new_vm(
970+
"RemoteVM", name="remote-vm", label="blue"
971+
)
972+
remotevm.remote_name = "myawesomevm"
973+
974+
holder = MyTestHolder(None)
975+
holder.relayvm = "local-relay"
976+
holder.transport_rpc = "qubesair.SSHProxy"
977+
self.assertEqual(holder.relayvm, "local-relay")
978+
self.assertEqual(holder.transport_rpc, "qubesair.SSHProxy")
979+
980+
self.assertEventFired(
981+
holder,
982+
"property-set:relayvm",
983+
kwargs={"name": "relayvm", "newvalue": "local-relay"},
984+
)
985+
986+
self.assertEventFired(
987+
holder,
988+
"property-set:transport_rpc",
989+
kwargs={"name": "transport_rpc", "newvalue": "qubesair.SSHProxy"},
990+
)
991+
992+
# Set RelayVM
993+
remotevm.relayvm = localrelay
994+
self.assertIn("relayvm-local-relay", remotevm.tags)
995+
996+
# Read QDB path
997+
self.assertEqual(
998+
localrelay.untrusted_qdb.read("/remote/remote-vm"),
999+
remotevm.remote_name,
1000+
)
1001+
9501002
def test_200_remove_template(self):
9511003
appvm = self.app.add_new_vm(
9521004
"AppVM", name="test-vm", template=self.template, label="red"

qubes/vm/remotevm.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The Qubes OS Project, http://www.qubes-os.org
22
#
3-
# Copyright (C) 2024 Frédéric Pierret (fepitre) <frederic@invisiblethingslab.com>
3+
# Copyright (C) 2024 Frédéric Pierret <frederic@invisiblethingslab.com>
44
#
55
# This program is free software; you can redistribute it and/or modify
66
# it under the terms of the GNU General Public License as published by
@@ -17,11 +17,6 @@
1717
#
1818
# SPDX-License-Identifier: GPL-3.0-or-later
1919

20-
import asyncio
21-
import grp
22-
import subprocess
23-
import uuid
24-
2520
import qubes
2621
import qubes.exc
2722
import qubes.vm
@@ -30,9 +25,31 @@
3025

3126
class RemoteVM(BaseVM):
3227

28+
relayvm = qubes.VMProperty(
29+
"relayvm",
30+
load_stage=4,
31+
allow_none=True,
32+
default=None,
33+
doc="Local qube used as relay.",
34+
)
35+
36+
transport_rpc = qubes.property(
37+
"transport_rpc",
38+
type=str,
39+
default=None,
40+
doc="Transport RPC used by the relay.",
41+
)
42+
43+
remote_name = qubes.property(
44+
"remote_name",
45+
type=str,
46+
default=None,
47+
doc="Name on the remote host.",
48+
)
49+
3350
def __init__(self, app, xml, **kwargs):
3451
super().__init__(app, xml, **kwargs)
35-
self.connected_relay_vm = None
52+
3653
if xml is None:
3754
self.events_enabled = True
3855
self.fire_event("domain-init")

rpm_spec/core-dom0.spec.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ done
404404
%{python3_sitelib}/qubes/vm/appvm.py
405405
%{python3_sitelib}/qubes/vm/dispvm.py
406406
%{python3_sitelib}/qubes/vm/qubesvm.py
407+
%{python3_sitelib}/qubes/vm/remotevm.py
407408
%{python3_sitelib}/qubes/vm/standalonevm.py
408409
%{python3_sitelib}/qubes/vm/templatevm.py
409410

@@ -447,6 +448,7 @@ done
447448
%{python3_sitelib}/qubes/ext/gui.py
448449
%{python3_sitelib}/qubes/ext/audio.py
449450
%{python3_sitelib}/qubes/ext/pci.py
451+
%{python3_sitelib}/qubes/ext/relay.py
450452
%{python3_sitelib}/qubes/ext/r3compatibility.py
451453
%{python3_sitelib}/qubes/ext/services.py
452454
%{python3_sitelib}/qubes/ext/supported_features.py

setup.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,20 @@ def run(self):
6262
],
6363
'qubes.ext': [
6464
'qubes.ext.admin = qubes.ext.admin:AdminExtension',
65+
'qubes.ext.audio = qubes.ext.audio:AUDIO',
6566
'qubes.ext.backup_restore = '
6667
'qubes.ext.backup_restore:BackupRestoreExtension',
68+
'qubes.ext.block = qubes.ext.block:BlockDeviceExtension',
6769
'qubes.ext.core_features = qubes.ext.core_features:CoreFeatures',
6870
'qubes.ext.custom_persist = qubes.ext.custom_persist:CustomPersist',
6971
'qubes.ext.gui = qubes.ext.gui:GUI',
70-
'qubes.ext.audio = qubes.ext.audio:AUDIO',
71-
'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility',
7272
'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension',
73-
'qubes.ext.block = qubes.ext.block:BlockDeviceExtension',
73+
'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility',
74+
'qubes.ext.relay = qubes.ext.relay:Relay',
7475
'qubes.ext.services = qubes.ext.services:ServicesExtension',
7576
'qubes.ext.supported_features = qubes.ext.supported_features:SupportedFeaturesExtension',
76-
'qubes.ext.windows = qubes.ext.windows:WindowsFeatures',
7777
'qubes.ext.vm_config = qubes.ext.vm_config:VMConfig',
78+
'qubes.ext.windows = qubes.ext.windows:WindowsFeatures',
7879
],
7980
'qubes.devices': [
8081
'pci = qubes.ext.pci:PCIDevice',

0 commit comments

Comments
 (0)