Commit 4f5847db authored by anonym's avatar anonym Committed by intrigeri

Remote shell: switch from serial to virtio transport (refs: #11888).

This results in a massive ~1000x throughput improvement over the
remote shell's communication channel! Previously dumping the Tor log
or the journal as failure artifacts were painfully slow, but no more!
parent 7c9421d6
......@@ -10,7 +10,6 @@ import io
import json
import os
import pwd
import serial
import signal
import subprocess
import sys
......@@ -18,7 +17,7 @@ import systemd.daemon
import textwrap
import traceback
REMOTE_SHELL_DEV = '/dev/ttyS0'
REMOTE_SHELL_DEV = '/dev/virtio-ports/org.tails.remote_shell.0'
def mk_switch_user_fn(user):
......@@ -123,15 +122,44 @@ def run_cmd_as_user(cmd, user):
def main():
port = serial.Serial(port = REMOTE_SHELL_DEV, baudrate = 4000000)
python_sessions = dict()
fd = os.open(REMOTE_SHELL_DEV, os.O_RDWR)
port = open(fd, 'wb+', buffering=0)
# In order to avoid busy-waiting when polling the above character
# device for new data sent from clients, we borrow the approach used
# by python-negotiator (https://github.com/xolox/python-negotiator):
# We add O_ASYNC so a SIGIO signal is sent to us whenever data is
# ready to be read from the device.
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_ASYNC)
fcntl.fcntl(fd, fcntl.F_SETOWN, os.getpid())
# By default receiving a SIGIO terminates the process so we override
# it to do nothing instead.
signal.signal(signal.SIGIO, lambda *args: None)
# Notify systemd that we're ready
systemd.daemon.notify('READY=1')
systemd.daemon.notify('STATUS=Processing requests...\n')
while True:
# We can avoid an unnecessary delay of up to one second during the
# first iteration of this loop; if a client sends a request
# before we run SETOWN above, the expected signal never
# reaches us, and we enter the loop and pass through the
# conditional doomed to a full second of mournful waiting
# for this lost signal. :_( By trying to read before we do a
# timed wait for the signal, this delay is avoided.
line = port.readline().decode('utf-8')
if line == "":
# In case the SIGIO gets lost for whatever reason
# (e.g. the one mentioned above), let's always poll at
# least once every second.
try:
signal.sigtimedwait([signal.SIGIO], 1)
except InterruptedError:
# Thrown if any other signal is received.
pass
continue
try:
id, cmd_type, *rest = json.loads(line)
ret = ""
......@@ -172,7 +200,13 @@ def main():
else:
raise ValueError("unknown command type")
response = (ret + "\n").encode('utf-8')
port.write(response)
# We can only write 2**15 bytes at a time to the virtio channel
# (seems to only affect the guest -> host direction).
chunk_size = 2**15
chunks = (response[0+i:chunk_size+i] for i in \
range(0, len(response), chunk_size))
for chunk in chunks:
port.write(chunk)
port.flush()
except Exception as e:
print("Error caught while processing line:", file=sys.stderr)
......
......@@ -28,10 +28,6 @@
<model type='virtio'/>
<link state='up'/>
</interface>
<serial type='tcp'>
<source mode="bind" host='127.0.0.1' service='1337'/>
<target port='0'/>
</serial>
<input type='tablet' bus='usb'/>
<channel type='spicevmc'>
<target type='virtio' name='com.redhat.spice.0'/>
......
#!/usr/bin/env ruby
require 'libvirt'
require 'optparse'
require 'rexml/document'
begin
top_level_dir = `git rev-parse --show-toplevel`.chomp
require "#{top_level_dir}/features/support/helpers/remote_shell.rb"
......@@ -13,8 +15,29 @@ $config = {}
def debug_log(*args); end
class FakeVM
def get_remote_shell_port
1337
def initialize
domain = nil
@domain_xml = ""
begin
virt = Libvirt::open("qemu:///system")
domain = virt.lookup_domain_by_name("TailsToaster")
@domain_xml = domain.xml_desc
rescue
raise "There was a problem with the TailsToaster VM (is it running?)"
ensure
virt.close
end
end
def remote_shell_socket_path
rexml = REXML::Document.new(@domain_xml)
rexml.elements.each('domain/devices/channel') do |e|
target = e.elements['target']
if target.attribute('name').to_s == 'org.tails.remote_shell.0'
return e.elements['source'].attribute('path').to_s
end
end
raise "The running TailsToaster has no remote shell channel!"
end
end
......
......@@ -7,6 +7,12 @@ module RemoteShell
class ServerFailure < StandardError
end
# This exception is *only* supposed to be use internally in
# communicate() -- in particular it must not be raised by a
# Timeout.timeout() wrapping around communicate() or any use of it.
class SocketReadTimeout < Exception
end
# Used to differentiate vs Timeout::Error, which is thrown by
# try_for() (by default) and often wraps around remote shell usage
# -- in that case we don't want to catch that "outer" exception in
......@@ -22,7 +28,7 @@ module RemoteShell
def communicate(vm, *args, **opts)
opts[:timeout] ||= DEFAULT_TIMEOUT
socket = TCPSocket.new('127.0.0.1', vm.get_remote_shell_port)
socket = UNIXSocket.new(vm.remote_shell_socket_path)
id = (@@request_id += 1)
# Since we already have defined our own Timeout in the current
# scope, we have to be more careful when referring to the Timeout
......@@ -33,7 +39,7 @@ module RemoteShell
socket.flush
loop do
line = socket.readline("\n").chomp("\n")
response_id, status, *rest = JSON.parse(line)
response_id, status, *rest = JSON.load(line)
if response_id == id
if status != 'success'
# rubocop:disable Style/GuardClause
......
......@@ -89,6 +89,7 @@ class VM
@display = Display.new(@domain_name, x_display)
set_cdrom_boot(TAILS_ISO)
plug_network
add_remote_shell_channel
rescue StandardError => e
destroy_and_undefine
raise e
......@@ -434,6 +435,37 @@ class VM
execute(cmd, **options)
end
def remote_shell_socket_path
domain_rexml = REXML::Document.new(@domain.xml_desc)
domain_rexml.elements.each('domain/devices/channel') do |e|
target = e.elements['target']
if target.attribute('name').to_s == 'org.tails.remote_shell.0'
return e.elements['source'].attribute('path').to_s
end
end
return nil
end
def add_remote_shell_channel
if running?
raise "The remote shell channel can only be added for inactive vms"
end
if @remote_shell_socket_path.nil?
@remote_shell_socket_path =
"/tmp/remote-shell_" + random_alnum_string(8) + ".socket"
end
channel_xml = <<-EOF
<channel type='unix'>
<source mode="bind" path='#{@remote_shell_socket_path}'/>
<target type='virtio' name='org.tails.remote_shell.0'/>
</channel>
EOF
channel_rexml = REXML::Document.new(channel_xml)
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/devices'].add_element(channel_rexml)
update(xml: domain_xml.to_s)
end
def remote_shell_is_up?
msg = 'hello?'
Timeout.timeout(3) do
......@@ -718,18 +750,11 @@ class VM
@display.stop
end
def get_remote_shell_port
domain_xml.elements.each('domain/devices/serial') do |e|
if e.attribute('type').to_s == 'tcp'
return e.elements['source'].attribute('service').to_s.to_i
end
end
end
def set_vcpu(nr_cpus)
raise 'Cannot set the number of CPUs for a running domain' if running?
update { |xml| xml.elements['domain/vcpu'].text = nr_cpus }
end
end
# rubocop:enable Metrics/ClassLength
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment