2015-04-24 12:01:43 +03:00
# Copyright (C) 2014, 2015 Red Hat, Inc.
2014-09-21 21:36:28 +04:00
#
2018-04-04 16:35:41 +03:00
# This work is licensed under the GNU GPLv2 or later.
2018-03-20 22:00:02 +03:00
# See the COPYING file in the top-level directory.
2014-09-21 21:36:28 +04:00
2017-10-11 14:35:59 +03:00
import functools
2014-09-21 21:36:28 +04:00
import os
2017-10-11 14:35:53 +03:00
import queue
2014-09-21 21:36:28 +04:00
import socket
import signal
import threading
2017-10-11 14:35:48 +03:00
import ipaddress
2014-09-21 21:36:28 +04:00
2019-06-17 04:12:39 +03:00
from virtinst import log
2019-06-17 04:56:34 +03:00
from . . baseclass import vmmGObject
2014-09-21 21:36:28 +04:00
class ConnectionInfo ( object ) :
"""
Holds all the bits needed to make a connection to a graphical console
"""
2024-03-03 00:22:53 +03:00
def __init__ ( self , conn , gdev ) :
2016-05-08 00:36:42 +03:00
self . gtype = gdev . type
2024-03-03 00:22:53 +03:00
self . gidx = gdev . get_xml_idx ( )
2022-12-13 18:51:14 +03:00
self . gport = str ( gdev . port ) if gdev . port else None
2017-09-06 10:24:53 +03:00
self . gsocket = ( gdev . listens and gdev . listens [ 0 ] . socket ) or gdev . socket
2016-05-08 00:36:42 +03:00
self . gaddr = gdev . listen or " 127.0.0.1 "
self . gtlsport = gdev . tlsPort or None
2017-04-27 22:28:41 +03:00
self . glistentype = gdev . get_first_listen_type ( )
2014-09-21 21:36:28 +04:00
2015-04-11 19:08:57 +03:00
self . transport = conn . get_uri_transport ( )
self . connuser = conn . get_uri_username ( )
2014-09-21 21:36:28 +04:00
2015-04-11 19:57:32 +03:00
self . _connhost = conn . get_uri_hostname ( ) or " localhost "
2015-04-11 19:08:57 +03:00
self . _connport = conn . get_uri_port ( )
2014-09-21 21:36:28 +04:00
if self . _connhost == " localhost " :
self . _connhost = " 127.0.0.1 "
def _is_listen_localhost ( self , host = None ) :
2015-05-06 13:52:40 +03:00
try :
2018-01-27 22:19:12 +03:00
return ipaddress . ip_network ( str ( host or self . gaddr ) ) . is_loopback
2017-07-24 11:26:48 +03:00
except Exception :
2015-05-06 13:52:40 +03:00
return False
2014-09-21 21:36:28 +04:00
def _is_listen_any ( self ) :
2015-05-06 13:52:40 +03:00
try :
2018-01-27 22:19:12 +03:00
return ipaddress . ip_network ( str ( self . gaddr ) ) . is_unspecified
2017-07-24 11:26:48 +03:00
except Exception :
2015-05-06 13:52:40 +03:00
return False
2014-09-21 21:36:28 +04:00
2016-05-18 23:57:38 +03:00
def _is_listen_none ( self ) :
2017-04-27 22:28:41 +03:00
if self . glistentype == " none " :
return True
2016-05-18 23:57:38 +03:00
return not ( self . gsocket or self . gport or self . gtlsport )
2014-09-21 21:36:28 +04:00
def need_tunnel ( self ) :
if not self . _is_listen_localhost ( ) :
return False
2016-05-18 23:05:04 +03:00
return self . transport == " ssh "
2014-09-21 21:36:28 +04:00
2016-05-18 23:57:38 +03:00
def bad_config ( self ) :
if self . transport and self . _is_listen_none ( ) :
return _ ( " Guest is on a remote host, but is only configured "
" to allow local file descriptor connections. " )
if self . need_tunnel ( ) and ( self . gtlsport and not self . gport ) :
return _ ( " Guest is configured for TLS only which does not "
" work over SSH. " )
if ( not self . need_tunnel ( ) and
self . transport and
2023-10-09 14:53:11 +03:00
self . _is_listen_localhost ( ) and
not self . _is_listen_localhost ( self . _connhost ) ) :
2016-05-18 23:57:38 +03:00
return _ ( " Guest is on a remote host with transport ' %s ' "
" but is only configured to listen locally. "
" To connect remotely you will need to change the guest ' s "
" listen address. " % self . transport )
2014-09-21 21:36:28 +04:00
def get_conn_host ( self ) :
2016-05-16 21:52:35 +03:00
"""
Return host / port / tlsport needed for direct spice / vnc connection
"""
host = self . gaddr
port = self . gport
tlsport = self . gtlsport
if self . _is_listen_any ( ) :
host = self . _connhost
2014-09-21 21:36:28 +04:00
return host , port , tlsport
2016-05-16 21:52:35 +03:00
def get_tunnel_host ( self ) :
"""
Physical host name / port needed for ssh tunnel connection
"""
return self . _connhost , self . _connport
2014-09-21 21:36:28 +04:00
def logstring ( self ) :
return ( " proto= %s trans= %s connhost= %s connuser= %s "
" connport= %s gaddr= %s gport= %s gtlsport= %s gsocket= %s " %
( self . gtype , self . transport , self . _connhost , self . connuser ,
self . _connport , self . gaddr , self . gport , self . gtlsport ,
self . gsocket ) )
class _TunnelScheduler ( object ) :
"""
If the user is using Spice + SSH URI + no SSH keys , we need to
serialize connection opening otherwise ssh - askpass gets all angry .
This handles the locking and scheduling .
It ' s only instantiated once for the whole app, because we serialize
independent of connection , vm , etc .
"""
def __init__ ( self ) :
2015-04-12 04:39:24 +03:00
self . _thread = None
2017-10-11 14:35:53 +03:00
self . _queue = queue . Queue ( )
2014-09-21 21:36:28 +04:00
self . _lock = threading . Lock ( )
def _handle_queue ( self ) :
while True :
2015-04-12 04:39:24 +03:00
lock_cb , cb , args , = self . _queue . get ( )
lock_cb ( )
2014-09-21 21:36:28 +04:00
vmmGObject . idle_add ( cb , * args )
2015-04-12 04:39:24 +03:00
def schedule ( self , lock_cb , cb , * args ) :
if not self . _thread :
self . _thread = threading . Thread ( name = " Tunnel thread " ,
target = self . _handle_queue ,
args = ( ) )
self . _thread . daemon = True
2014-09-21 21:36:28 +04:00
if not self . _thread . is_alive ( ) :
self . _thread . start ( )
2015-04-12 04:39:24 +03:00
self . _queue . put ( ( lock_cb , cb , args ) )
2014-09-21 21:36:28 +04:00
def lock ( self ) :
self . _lock . acquire ( )
def unlock ( self ) :
self . _lock . release ( )
2015-04-12 04:39:24 +03:00
_tunnel_scheduler = _TunnelScheduler ( )
2014-09-21 21:36:28 +04:00
class _Tunnel ( object ) :
def __init__ ( self ) :
2015-04-12 04:39:24 +03:00
self . _pid = None
self . _closed = False
2015-04-12 05:10:46 +03:00
self . _errfd = None
2014-09-21 21:36:28 +04:00
def close ( self ) :
2015-04-12 04:39:24 +03:00
if self . _closed :
2020-08-29 20:32:37 +03:00
return # pragma: no cover
2015-04-12 04:39:24 +03:00
self . _closed = True
2014-09-21 21:36:28 +04:00
2019-06-17 04:12:39 +03:00
log . debug ( " Close tunnel PID= %s ERRFD= %s " ,
2015-04-12 05:10:46 +03:00
self . _pid , self . _errfd and self . _errfd . fileno ( ) or None )
# Since this is a socket object, the file descriptor is closed
# when it's garbage collected.
2015-04-12 04:39:24 +03:00
self . _errfd = None
2014-09-21 21:36:28 +04:00
2015-04-12 04:39:24 +03:00
if self . _pid :
os . kill ( self . _pid , signal . SIGKILL )
os . waitpid ( self . _pid , 0 )
self . _pid = None
2014-09-21 21:36:28 +04:00
def get_err_output ( self ) :
errout = " "
while True :
try :
2015-04-12 04:39:24 +03:00
new = self . _errfd . recv ( 1024 )
2020-08-29 20:32:37 +03:00
except Exception : # pragma: no cover
2014-09-21 21:36:28 +04:00
break
if not new :
break
2018-08-22 14:29:52 +03:00
errout + = new . decode ( )
2014-09-21 21:36:28 +04:00
return errout
2015-04-12 19:17:55 +03:00
def open ( self , argv , sshfd ) :
2015-04-12 04:39:24 +03:00
if self . _closed :
2020-08-29 20:32:37 +03:00
return # pragma: no cover
2014-09-21 21:36:28 +04:00
2015-04-12 05:10:46 +03:00
errfds = socket . socketpair ( )
2014-09-21 21:36:28 +04:00
pid = os . fork ( )
2020-08-29 20:32:37 +03:00
if pid == 0 : # pragma: no cover
2015-04-12 05:10:46 +03:00
errfds [ 0 ] . close ( )
os . dup2 ( sshfd . fileno ( ) , 0 )
os . dup2 ( sshfd . fileno ( ) , 1 )
os . dup2 ( errfds [ 1 ] . fileno ( ) , 2 )
2014-09-21 21:36:28 +04:00
os . execlp ( * argv )
os . _exit ( 1 ) # pylint: disable=protected-access
2015-04-12 05:10:46 +03:00
sshfd . close ( )
errfds [ 1 ] . close ( )
self . _errfd = errfds [ 0 ]
self . _errfd . setblocking ( 0 )
2019-06-17 04:12:39 +03:00
log . debug ( " Opened tunnel PID= %d ERRFD= %d " ,
2015-04-12 05:10:46 +03:00
pid , self . _errfd . fileno ( ) )
2014-09-21 21:36:28 +04:00
2015-04-12 04:39:24 +03:00
self . _pid = pid
2014-09-21 21:36:28 +04:00
2015-04-12 19:17:55 +03:00
def _make_ssh_command ( ginfo ) :
if not ginfo . need_tunnel ( ) :
return None
2016-05-16 21:52:35 +03:00
host , port = ginfo . get_tunnel_host ( )
2015-04-12 19:17:55 +03:00
# Build SSH cmd
argv = [ " ssh " , " ssh " ]
if port :
argv + = [ " -p " , str ( port ) ]
if ginfo . connuser :
argv + = [ ' -l ' , ginfo . connuser ]
argv + = [ host ]
# Build 'nc' command run on the remote host
#
# This ugly thing is a shell script to detect availability of
# the -q option for 'nc': debian and suse based distros need this
# flag to ensure the remote nc will exit on EOF, so it will go away
# when we close the VNC tunnel. If it doesn't go away, subsequent
# VNC connection attempts will hang.
#
# Fedora's 'nc' doesn't have this option, and apparently defaults
# to the desired behavior.
#
if ginfo . gsocket :
nc_params = " -U %s " % ginfo . gsocket
else :
nc_params = " %s %s " % ( ginfo . gaddr , ginfo . gport )
nc_cmd = (
""" nc -q 2>&1 | grep " requires an argument " >/dev/null; """
""" if [ $? -eq 0 ] ; then """
""" CMD= " nc -q 0 %(nc_params)s " ; """
""" else """
""" CMD= " nc %(nc_params)s " ; """
""" fi; """
""" eval " $CMD " ; """ %
{ ' nc_params ' : nc_params } )
argv . append ( " sh -c " )
argv . append ( " ' %s ' " % nc_cmd )
2017-10-11 14:35:59 +03:00
argv_str = functools . reduce ( lambda x , y : x + " " + y , argv [ 1 : ] )
2019-06-17 04:12:39 +03:00
log . debug ( " Pre-generated ssh command for ginfo: %s " , argv_str )
2015-04-12 19:17:55 +03:00
return argv
2014-09-21 21:36:28 +04:00
class SSHTunnels ( object ) :
def __init__ ( self , ginfo ) :
self . _tunnels = [ ]
2015-04-12 19:17:55 +03:00
self . _sshcommand = _make_ssh_command ( ginfo )
2015-04-12 04:39:24 +03:00
self . _locked = False
2014-09-21 21:36:28 +04:00
def open_new ( self ) :
t = _Tunnel ( )
self . _tunnels . append ( t )
2015-04-12 05:10:46 +03:00
# socket FDs are closed when the object is garbage collected. This
# can close an FD behind spice/vnc's back which causes crashes.
#
# Dup a bare FD for the viewer side of things, but keep the high
# level socket object for the SSH side, since it simplifies things
# in that area.
viewerfd , sshfd = socket . socketpair ( )
2015-04-12 19:17:55 +03:00
_tunnel_scheduler . schedule ( self . _lock , t . open , self . _sshcommand , sshfd )
2015-04-12 05:10:46 +03:00
retfd = os . dup ( viewerfd . fileno ( ) )
2019-06-17 04:12:39 +03:00
log . debug ( " Generated tunnel fd= %s for viewer " , retfd )
2015-04-12 05:10:46 +03:00
return retfd
2014-09-21 21:36:28 +04:00
def close_all ( self ) :
for l in self . _tunnels :
l . close ( )
2015-04-12 04:39:24 +03:00
self . _tunnels = [ ]
2015-04-12 20:04:32 +03:00
self . unlock ( )
2014-09-21 21:36:28 +04:00
def get_err_output ( self ) :
2016-05-16 23:22:35 +03:00
errstrings = [ ]
2014-09-21 21:36:28 +04:00
for l in self . _tunnels :
2016-05-16 23:22:35 +03:00
e = l . get_err_output ( ) . strip ( )
if e and e not in errstrings :
errstrings . append ( e )
return " \n " . join ( errstrings )
2014-09-21 21:36:28 +04:00
2015-04-12 04:39:24 +03:00
def _lock ( self ) :
_tunnel_scheduler . lock ( )
self . _locked = True
2014-09-21 21:36:28 +04:00
def unlock ( self , * args , * * kwargs ) :
2015-04-12 04:39:24 +03:00
if self . _locked :
_tunnel_scheduler . unlock ( * args , * * kwargs )
self . _locked = False