api/admin: make admin.vm.Console call go through qubesd

Ask qubesd for admin.vm.Console call. This allows to intercept it with
admin-permission event. While at it, extract tty path extraction to
python, where libvirt domain object is already available.

Fixes QubesOS/qubes-issues#5030
This commit is contained in:
Marek Marczykowski-Górecki 2019-06-09 18:03:18 +02:00
parent c5aaf8cdd7
commit b6c4f8456f
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
3 changed files with 77 additions and 4 deletions

View File

@ -1,11 +1,23 @@
#!/bin/bash
# TODO: handle 'admin-permission' event for qubesd
lock="/var/run/qubes/$QREXEC_REQUESTED_TARGET.terminal.lock"
qvm-check --quiet --running "$QREXEC_REQUESTED_TARGET" > /dev/null 2>&1 || { echo "Error: domain '$QREXEC_REQUESTED_TARGET' does not exist or is not running"; exit 1; }
# use temporary file, because env variables deal poorly with \0 inside
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
qubesd-query -e \
"$QREXEC_REMOTE_DOMAIN" \
"admin.vm.Console" \
"$QREXEC_REQUESTED_TARGET" \
"$1" >$tmpfile
# exit if qubesd returned an error (not '0\0')
if [ "$(head -c 2 $tmpfile | xxd -p)" != "3000" ]; then
cat "$tmpfile"
exit 1
fi
path=$(tail -c +3 "$tmpfile")
# Create an exclusive lock to ensure that multiple qubes cannot access to the same socket
# In the case of multiple qrexec calls it returns a specific exit code
sudo flock -n -E 200 -x "$lock" socat - OPEN:"$(virsh -c xen ttyconsole "$QREXEC_REQUESTED_TARGET")"
sudo flock -n -E 200 -x "$lock" socat - OPEN:"$path"

View File

@ -29,6 +29,7 @@ import string
import subprocess
import libvirt
import lxml.etree
import pkg_resources
import yaml
@ -575,6 +576,23 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
self.app.save()
@qubes.api.method('admin.vm.Console', no_payload=True,
scope='local', write=True)
@asyncio.coroutine
def vm_console(self):
self.enforce(not self.arg)
self.fire_event_for_permission()
if not self.dest.is_running():
raise qubes.exc.QubesVMNotRunningError(self.dest)
xml_desc = lxml.etree.fromstring(self.dest.libvirt_domain.XMLDesc())
ttypath = xml_desc.xpath('string(/domain/devices/console/@tty)')
# this value is returned to /etc/qubes-rpc/admin.vm.Console script,
# which will call socat on it
return ttypath
@qubes.api.method('admin.pool.List', no_payload=True,
scope='global', read=True)
@asyncio.coroutine

View File

@ -2475,6 +2475,47 @@ class TC_00_VMs(AdminAPITestCase):
b'test-vm1', b'private', b'abc')
self.assertFalse(self.app.save.called)
def test_690_vm_console(self):
self.vm._libvirt_domain = unittest.mock.Mock()
xml_desc = (
'<domain type=\'xen\' id=\'42\'>\n'
'<name>test-vm1</name>\n'
'<devices>\n'
'<console type=\'pty\' tty=\'/dev/pts/42\'>\n'
'<source path=\'/dev/pts/42\'/>\n'
'<target type=\'xen\' port=\'0\'/>\n'
'</console>\n'
'</devices>\n'
'</domain>\n'
)
self.vm._libvirt_domain.configure_mock(
**{'XMLDesc.return_value': xml_desc,
'isActive.return_value': True}
)
self.app.vmm.configure_mock(offline_mode=False)
value = self.call_mgmt_func(b'admin.vm.Console', b'test-vm1')
self.assertEqual(value, '/dev/pts/42')
def test_691_vm_console_not_running(self):
self.vm._libvirt_domain = unittest.mock.Mock()
xml_desc = (
'<domain type=\'xen\' id=\'42\'>\n'
'<name>test-vm1</name>\n'
'<devices>\n'
'<console type=\'pty\' tty=\'/dev/pts/42\'>\n'
'<source path=\'/dev/pts/42\'/>\n'
'<target type=\'xen\' port=\'0\'/>\n'
'</console>\n'
'</devices>\n'
'</domain>\n'
)
self.vm._libvirt_domain.configure_mock(
**{'XMLDesc.return_value': xml_desc,
'isActive.return_value': False}
)
with self.assertRaises(qubes.exc.QubesVMNotRunningError):
self.call_mgmt_func(b'admin.vm.Console', b'test-vm1')
def test_990_vm_unexpected_payload(self):
methods_with_no_payload = [
b'admin.vm.List',
@ -2505,6 +2546,7 @@ class TC_00_VMs(AdminAPITestCase):
b'admin.vm.Pause',
b'admin.vm.Unpause',
b'admin.vm.Kill',
b'admin.vm.Console',
b'admin.Events',
b'admin.vm.feature.List',
b'admin.vm.feature.Get',
@ -2549,6 +2591,7 @@ class TC_00_VMs(AdminAPITestCase):
b'admin.vm.Pause',
b'admin.vm.Unpause',
b'admin.vm.Kill',
b'admin.vm.Console',
b'admin.Events',
b'admin.vm.feature.List',
]