x11 forwarding with paramiko

jcollado picture jcollado · Sep 10, 2012 · Viewed 6.9k times · Source

I'm trying to run a command with paramiko that should be able to open an X window. The script I'm using would something as follows:

import paramiko                                    

ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect('192.168.122.55', username='user', password='password')
transport = ssh_client.get_transport()
session = transport.open_session()

session.request_x11()
stdin = session.makefile('wb')
stdout = session.makefile('rb')
stderr = session.makefile_stderr('rb')
session.exec_command('env; xterm')
transport.accept()

print 'Exit status:', session.recv_exit_status()
print 'stdout:\n{}'.format(stdout.read())
print 'stderr:\n{}'.format(stderr.read())
session.close()

Unfortunately, when I run the script above, I get this output:

Exit status: 1
stdout:
SHELL=/bin/bash
XDG_SESSION_COOKIE=8025e1ba5e6c47be0d2f3ad6504a25ee-1347286654.617967-1932974971
SSH_CLIENT=192.168.122.1 58654 22
USER=user
MAIL=/var/mail/user
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
PWD=/home/user
LANG=en_US.UTF-8
SHLVL=1
HOME=/home/user
LOGNAME=user
SSH_CONNECTION=192.168.122.1 58654 192.168.122.55 22
DISPLAY=localhost:10.0
_=/usr/bin/env

stderr:  
xterm: Xt error: Can't open display: localhost:10.0

If I run the following command in a terminal:

ssh -X [email protected] 'env; xterm'

then I get the same environment variables (some ports changed, though), so I'd say that my environment is correct. However, I'm still missing something to make paramiko work with x11 forwarding.

A couple of things I tried are:

  • Use the handler parameter in request_x11: aside from printing values, I didn't get any further than with the default handler.
  • Use the auth_cookie parameter in request_x11: tried to hardcode a cookie value that was being used according to the xauth list output. The idea of doing this was to avoid problems that might happen according to the documentation string in paramiko itself:

If you omit the auth_cookie, a new secure random 128-bit value will be generated, used, and returned. You will need to use this value to verify incoming x11 requests and replace them with the actual local x11 cookie (which requires some knoweldge of the x11 protocol).

Is there some other thing I could do to make it work or troubleshoot the problem?

Note: This has been previously asked in:

  • superuser: the only response points to the request_x11 documentation I've already tried to use to no avail.
  • stackoverflow: the accepted response suggests to use the handler parameter, but it's wrong.
  • github: no answer provided for more than a year.

Answer

Gary van der Merwe picture Gary van der Merwe · Oct 15, 2012

Reading the paramiko code, I realized that paramiko only implements a way to establish an x11 channel. It does not connect the channel to the local x11 display. That is left to you.

Here is a small implementation that I have just written:

#!/usr/bin/env python

import os
import select
import sys

import paramiko
import Xlib.support.connect as xlib_connect


local_x11_display = xlib_connect.get_display(os.environ['DISPLAY'])
local_x11_socket = xlib_connect.get_socket(*local_x11_display[:3])


ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect('server', username='username', password='password')
transport = ssh_client.get_transport()
session = transport.open_session()
session.request_x11(single_connection=True)
session.exec_command('xterm')
x11_chan = transport.accept()

session_fileno = session.fileno()
x11_chan_fileno = x11_chan.fileno()
local_x11_socket_fileno = local_x11_socket.fileno()

poller = select.poll()
poller.register(session_fileno, select.POLLIN)
poller.register(x11_chan_fileno, select.POLLIN)
poller.register(local_x11_socket, select.POLLIN)
while not session.exit_status_ready():
    poll = poller.poll()
    if not poll: # this should not happen, as we don't have a timeout.
        break
    for fd, event in poll:
        if fd == session_fileno:
            while session.recv_ready():
                sys.stdout.write(session.recv(4096))
            while session.recv_stderr_ready():
                sys.stderr.write(session.recv_stderr(4096))
        if fd == x11_chan_fileno:
            local_x11_socket.sendall(x11_chan.recv(4096))
        if fd == local_x11_socket_fileno:
            x11_chan.send(local_x11_socket.recv(4096))

print 'Exit status:', session.recv_exit_status()
while session.recv_ready():
    sys.stdout.write(session.recv(4096))
while session.recv_stderr_ready():
    sys.stdout.write(session.recv_stderr(4096))
session.close()

Some notes:

  • I'm using some helper functions from python-Xlib. This is a pure python implementation of Xlib. See this question for details on installing it: How do you install Python Xlib with pip?

  • Some of the details of how I have implemented this make me believe it will only work for 1 x11 connection (hence session.request_x11(single_connection=True).) I would like to keep working at this to get it to handle multiple connections, but that will have to wait for another day.

  • This code essentially connects the following channels/sockets together in a async fashion using select.poll:

    • session.stdout -> sys.stdout
    • session.stderr -> sys.stderr
    • x11channel -> local_x11_socket
    • local_x11_socket - > x11channel
  • The paramiko module outputs alot of usefull debuging info to the logging module. You can view this by configuring the logging module:

    import logging
    logging.basicConfig(level=logging.DEBUG)