使用Paramiko进行X11转发

18
我正在尝试使用paramiko运行一个可以打开X窗口的命令。我使用的脚本应该如下所示:
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()

很不幸,当我运行上面的脚本时,我得到了以下输出:
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

如果我在终端中运行以下命令:
ssh -X user@192.168.122.55 'env; xterm'

然后我得到了相同的环境变量(一些端口已更改),所以我认为我的环境是正确的。然而,我仍然缺少一些东西来使paramiko与x11转发配合使用。
我尝试过的几件事情是:
  • request_x11中使用handler参数:除了打印值之外,我没有比默认处理程序更进一步。
  • request_x11中使用auth_cookie参数:尝试硬编码一个cookie值,该值根据xauth list输出正在使用。这样做的想法是避免根据paramiko自身的文档字符串可能发生的问题:

如果省略auth_cookie,则会生成、使用和返回新的安全随机128位值。您需要使用此值来验证传入的x11请求,并用实际的本地x11 cookie替换它们(这需要一些关于x11协议的知识)。

还有其他什么我可以做来使它工作或解决问题吗?
注意: 这个问题之前已经被问过:
  • superuser: 唯一的回答指向我已经尝试过但无济于事的request_x11文档。
  • stackoverflow: 接受的回答建议使用handler参数,但是是错误的。
  • github: 一年多没有提供答案。

1
有了paramico会话活动:(1)在远程计算机上以root身份运行netstat -lpn。它是否在端口6010上侦听,地址为127.0.0.1?(2)ls -l ~user/.Xauthority显示什么? - n. m.
5个回答

21

阅读paramiko代码,我意识到paramiko只实现了建立x11通道的方式。它没有将通道连接到本地x11显示器上,这需要你自己完成。

以下是我刚刚编写的一个小实现:

#!/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()

一些注意事项:
  • 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)
    

1
感谢您的回复。我一直在看您的示例;但是,尽管很有前途,似乎对我尝试的其他命令无效。例如,如果我尝试 gnome-terminal,我会得到 Autolaunch error: X11 initialization failed.,如果我尝试 firefox,我会得到 Error: cannot open display: localhost:10.0 - jcollado
1
另外两个答案声称是基于你的,所以你获得赏金是公平的。再次感谢。 - jcollado
感谢大家提供的代码片段。我有两个评论,首先是在0.26版本中,python-xlib的“get_socket”函数需要4个参数,因此调用应该是“xlib_connect.get_socket(*local_x11_display[:4])”。其次,在X11握手期间,我遇到了“Invalid MIT-MAGIC-COOKIE-1 keyError”的错误,所以我不得不使用“xhost +”打开访问权限,这通常在使用“ssh -X”时是不必要的。 - Michael Dussere

2

鉴于您要求一个“最小”的版本,我理解为使其尽可能易于使用。这是一个基于两种代码的版本,但将x11会话命令与通用代码分离,使主程序简单且会话代码可重复使用:

import paramiko
import os
import select
import sys
import Xlib.support.connect as xlib_connect

def run(transport, session, command):
    def x11_handler(channel, (src_addr, src_port)):
        x11_fileno = channel.fileno()
        local_x11_channel = xlib_connect.get_socket(*local_x11_display[:3])
        local_x11_fileno = local_x11_channel.fileno()

        # Register both x11 and local_x11 channels
        channels[x11_fileno] = channel, local_x11_channel
        channels[local_x11_fileno] = local_x11_channel, channel

        poller.register(x11_fileno, select.POLLIN)
        poller.register(local_x11_fileno, select.POLLIN)

        transport._queue_incoming_channel(channel)

    def flush_out(channel):
        while channel.recv_ready():
            sys.stdout.write(channel.recv(4096))
        while channel.recv_stderr_ready():
            sys.stderr.write(channel.recv_stderr(4096))

    local_x11_display = xlib_connect.get_display(os.environ['DISPLAY'])

    channels = {}
    poller = select.poll()
    session_fileno = session.fileno()
    poller.register(session_fileno)

    session.request_x11(handler=x11_handler)
    session.exec_command(command)
    transport.accept()

    # event loop
    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:
                flush_out(session)
            # data either on local/remote x11 channels/sockets
            if fd in channels.keys():
                sender, receiver = channels[fd]
                try:
                    receiver.sendall(sender.recv(4096))
                except:
                    sender.close()
                    receiver.close()
                    channels.remove(fd)

    flush_out(session)
    return session.recv_exit_status()

if __name__ == '__main__':
    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()
    run(transport, session, 'xterm')

我知道你可以自己做到这点。但只需复制函数run,任何人都可以轻松使用它。
正确的答案在https://dev59.com/GWct5IYBdhLWcg3wF5tF#12903844。这个例子是为了让新手更容易理解。

谢谢您的回复。您尝试过除了 xterm 以外的其他命令吗?我已经尝试过 gnome-terminalfirefox,但两者似乎都会永远等待(根据我在按下 Ctrl+C 时得到的回溯信息,没有特定的位置)。 - jcollado

2
  • x11 请求可能使用 MIT-MAGIC-COOKIE-1,您可能不能正确处理
  • 直接使用 ssh,我发现它需要确认 x11 请求(cookie 挑战?)
  • .Xauthority 文件也可能是一个问题
  • 您可以尝试 strace ssh 进程并查看正常的流程
  • 在您的脚本中,您可以将 xterm 替换为 strace xterm 并与上面进行比较。

一些链接:

祝你好运。

编辑: 基于 Gary 的答案,使用多个 x11 连接。

#!/usr/bin/env python

import os
import select
import sys
import getpass
import paramiko
import socket
import logging
import Xlib.support.connect as xlib_connect
LOGGER = logging.getLogger(__name__)

# connection settings
host = '192.168.122.55'
user = 'user'
password = getpass.getpass()

ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect(host, username=user, password=password)
del password

# maintain map
# { fd: (channel, remote channel), ... }
channels = {}

poller = select.poll()
def x11_handler(channel, (src_addr, src_port)):
    '''handler for incoming x11 connections
    for each x11 incoming connection,
    - get a connection to the local display
    - maintain bidirectional map of remote x11 channel to local x11 channel
    - add the descriptors to the poller
    - queue the channel (use transport.accept())'''
    x11_chanfd = channel.fileno()
    local_x11_socket = xlib_connect.get_socket(*local_x11_display[:3])
    local_x11_socket_fileno = local_x11_socket.fileno()
    channels[x11_chanfd] = channel, local_x11_socket
    channels[local_x11_socket_fileno] = local_x11_socket, channel
    poller.register(x11_chanfd, select.POLLIN)
    poller.register(local_x11_socket, select.POLLIN)
    LOGGER.debug('x11 channel on: %s %s', src_addr, src_port)
    transport._queue_incoming_channel(channel)

def flush_out(session):
    while session.recv_ready():
        sys.stdout.write(session.recv(4096))
    while session.recv_stderr_ready():
        sys.stderr.write(session.recv_stderr(4096))

# get local disply
local_x11_display = xlib_connect.get_display(os.environ['DISPLAY'])
# start x11 session
transport = ssh_client.get_transport()
session = transport.open_session()
session.request_x11(handler=x11_handler)
session.exec_command('xterm')
session_fileno = session.fileno()
poller.register(session_fileno, select.POLLIN)
# accept first remote x11 connection
transport.accept()

# event loop
while not session.exit_status_ready():
    poll = poller.poll()
    # accept subsequent x11 connections if any
    if len(transport.server_accepts) > 0:
        transport.accept()
    if not poll: # this should not happen, as we don't have a timeout.
        break
    for fd, event in poll:
        if fd == session_fileno:
            flush_out(session)
        # data either on local/remote x11 socket
        if fd in channels.keys():
            channel, counterpart = channels[fd]
            try:
                # forward data between local/remote x11 socket.
                data = channel.recv(4096)
                counterpart.sendall(data)
            except socket.error:
                channel.close()
                counterpart.close()
                del channels[fd]

print 'Exit status:', session.recv_exit_status()
flush_out(session)
session.close()

你能提供一个最小化的示例代码并且保证它是可运行的吗? - jcollado
这是适合我的答案。我仍将示例翻译为实际代码,但我认为它会很有帮助,谢谢。 - jcollado

1

感谢Gary van der Merwednozay提供的代码。下面的代码在很大程度上依赖于它,并用于在Windows上运行X程序。值得注意的区别是使用select.select而不是poll,因为在Windows中没有poll可用。欢迎任何改进或更正。

import select
import sys
import paramiko
import Xlib.support.connect as xlib_connect
import os
import socket
import subprocess



# run xming
XmingProc = subprocess.Popen("C:/Program Files (x86)/Xming/Xming.exe :0 -clipboard -multiwindow")
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect(SSHServerIP, SSHServerPort, username=user, password=pwd)
transport = ssh_client.get_transport()
channelOppositeEdges = {}

local_x11_display = xlib_connect.get_display(os.environ['DISPLAY'])
inputSockets = []

def x11_handler(channel, (src_addr, src_port)):
    local_x11_socket = xlib_connect.get_socket(*local_x11_display[:3])
    inputSockets.append(local_x11_socket)
    inputSockets.append(channel)
    channelOppositeEdges[local_x11_socket.fileno()] = channel
    channelOppositeEdges[channel.fileno()] = local_x11_socket
    transport._queue_incoming_channel(channel)

session = transport.open_session()
inputSockets.append(session)
session.request_x11(handler = x11_handler)
session.exec_command('xterm')
transport.accept()

while not session.exit_status_ready():
    readable, writable, exceptional = select.select(inputSockets,[],[])
    if len(transport.server_accepts) > 0:
        transport.accept()
    for sock in readable:
        if sock is session:
            while session.recv_ready():
                sys.stdout.write(session.recv(4096))
            while session.recv_stderr_ready():
                sys.stderr.write(session.recv_stderr(4096))   
        else: 
            try:
                data = sock.recv(4096)
                counterPartSocket  = channelOppositeEdges[sock.fileno()]
                counterPartSocket.sendall(data)
            except socket.error:
                inputSockets.remove(sock)
                inputSockets.remove(counterPartSocket)
                del channelOppositeEdges[sock.fileno()]
                del channelOppositeEdges[counterPartSocket.fileno()]
                sock.close()
                counterPartSocket.close()

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()
XmingProc.terminate()
XmingProc.wait()

0

对于在 Mac OS X Leopard 上工作的人,没有 select.poll()。这里是 dnozay's answer 的修改版本,使用 kqueue 代替 poll。如有任何改进/更正,将不胜感激。

#!/usr/bin/env python

import os
import select
import sys
import paramiko
import socket
import Xlib.support.connect as xlib_connect

# get local display
local_x11_display = xlib_connect.get_display(os.environ['DISPLAY'])

ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect('hostname', port=22, username='username', password='password')

channels = {}
kq = select.kqueue()

def x11Handler(x11_chan, (src_addr, src_port)):
    x11_chan_fileno = x11_chan.fileno()
    local_x11_socket = xlib_connect.get_socket(*local_x11_display[:3])
    local_x11_socket_fileno = local_x11_socket.fileno()
    channels[x11_chan_fileno] = x11_chan, local_x11_socket
    channels[local_x11_socket_fileno] = local_x11_socket, x11_chan

    ev = [select.kevent(x11_chan_fileno, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD), select.kevent(local_x11_socket_fileno, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD)]
    kevents = kq.control(ev, 0, None)
    transport._queue_incoming_channel(x11_chan)

def flushOut(session):
    while session.recv_ready():
        sys.stdout.write(session.recv(4096))
    while session.recv_stderr_ready():
        sys.stderr.write(session.recv_stderr(4096))

# start x11 session
transport = ssh_client.get_transport()
session = transport.open_session()
session.request_x11(handler=x11Handler)
session.exec_command('xterm')

# accept first remote x11 connection
x11_chan = transport.accept()
session_fileno = session.fileno()

session_ev = [select.kevent(session_fileno, 
    filter=select.KQ_FILTER_READ,
    flags=select.KQ_EV_ADD)] 

kevents_session = kq.control(session_ev, 0, None)

# event loop
while not session.exit_status_ready():
    r_events = kq.control(None, 4)
    # accept subsequent x11 connections if any

    if len(transport.server_accepts) > 0:
        transport.accept()
    if not r_events: # this should not happen, as we don't have a timeout.
        break
    for event in r_events:
        print event
        if event.ident & session_fileno:
            flushOut(session)
        # data either on local/remote x11 socket
        if event.ident in channels.keys():
            x11_chan, counterpart = channels[event.ident]
            try:
                # forward data between local/remote x11 socket.
                data = x11_chan.recv(4096)
                counterpart.sendall(data)
            except socket.error:
                x11_chan.close()
                counterpart.close()
                del channels[event.ident]

flushOut(session)
kq.close()
session.close()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接