Paramiko SSH隧道关闭问题

4
我正在编写一个Python脚本,定期通过已建立的SSH隧道查询几个远程数据库。我对paramiko库比较熟悉,所以选择了这条路线。我希望保持完全使用Python,这样我就可以使用paramiko来处理密钥问题,并使用Python启动、控制和关闭SSH隧道。
这个话题在这里有一些相关的问题,但大多数答案都不完整。我的解决方案是从我找到的解决方案中拼凑出来的。
现在是问题:我能够很容易地创建第一个隧道(在单独的线程中),并进行我的DB/Python操作,但当尝试关闭隧道时,本地主机不会释放我绑定的本地端口。下面,我包含了我的源代码和每个步骤的相关netstat数据。
#!/usr/bin/python

import select
import SocketServer
import sys
import paramiko
from threading import Thread
import time



class ForwardServer(SocketServer.ThreadingTCPServer):
    daemon_threads = True
    allow_reuse_address = True

class Handler (SocketServer.BaseRequestHandler):
    def handle(self):
        try:
            chan = self.ssh_transport.open_channel('direct-tcpip', (self.chain_host, self.chain_port), self.request.getpeername())
        except Exception, e:
            print('Incoming request to %s:%d failed: %s' % (self.chain_host, self.chain_port, repr(e)))
            return
        if chan is None:
            print('Incoming request to %s:%d was rejected by the SSH server.' % (self.chain_host, self.chain_port))
            return
        print('Connected!  Tunnel open %r -> %r -> %r' % (self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port)))
        while True:
            r, w, x = select.select([self.request, chan], [], [])
            if self.request in r:
                data = self.request.recv(1024)
                if len(data) == 0:
                    break
                chan.send(data)
            if chan in r:
                data = chan.recv(1024)
                if len(data) == 0:
                    break
                self.request.send(data)
        chan.close()
        self.request.close()
        print('Tunnel closed from %r' % (self.request.getpeername(),))

class DBTunnel():

    def __init__(self,ip):
        self.c = paramiko.SSHClient()
        self.c.load_system_host_keys()
        self.c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.c.connect(ip, username='someuser')
        self.trans = self.c.get_transport()

    def startTunnel(self):
        class SubHandler(Handler):
            chain_host = '127.0.0.1'
            chain_port = 5432
            ssh_transport = self.c.get_transport()
        def ThreadTunnel():
            global t
            t = ForwardServer(('', 3333), SubHandler)
            t.serve_forever()
        Thread(target=ThreadTunnel).start()

    def stopTunnel(self):
        t.shutdown()
        self.trans.close()
        self.c.close()

虽然最终我会使用stopTunnel()类型的方法,但我意识到代码并不完全正确,而更多是试图关闭隧道并测试结果的实验。

当我首次调用create DBTunnel对象并调用startTunnel()时,netstat会产生以下结果:

tcp4       0      0 *.3333                 *.*                    LISTEN
tcp4       0      0 MYIP.36316      REMOTE_HOST.22                ESTABLISHED
tcp4       0      0 127.0.0.1.5432         *.*                    LISTEN

一旦我调用stopTunnel()方法,甚至删除DBTunnel对象本身..我将一直保留这个连接,直到我完全退出Python,然后我假设垃圾收集器会处理它:

tcp4       0      0 *.3333                 *.*                    LISTEN

希望找出为什么这个开放的套接字会独立于DBConnect对象而挂起,并学习如何从我的脚本中正确关闭它。如果我在完全退出Python之前尝试将不同的连接绑定到不同的IP使用相同的本地端口(time_wait不是问题),那么我会遇到臭名昭著的绑定错误48地址已被使用。谢谢提前 :)


1
上面的代码演示了原始paramiko转发示例代码中的一个错误,即self.request.getpeername()会导致坏文件描述符异常,因为它在请求关闭后调用。修复方法在这里:https://github.com/paramiko/paramiko/pull/36 - David Watson
4个回答

1
请注意,您不必像演示代码中所示那样执行Subhandler hack。该注释是错误的。处理程序确实可以访问其服务器的数据。在处理程序内部,您可以使用self.server.instance_data
如果您使用以下代码,在处理程序中,您将使用
  • self.server.chain_host
  • self.server.chain_port
  • self.server.ssh_transport

class ForwardServer(SocketServer.ThreadingTCPServer):
    daemon_threads = True
    allow_reuse_address = True

    def __init__(
          self, connection, handler, chain_host, chain_port, ssh_transport):
        SocketServer.ThreadingTCPServer.__init__(self, connection, handler)
        self.chain_host = chain_host
        self.chain_port = chain_port
        self.ssh_transport = ssh_transport
...

server = ForwardServer(('', local_port), Handler, 
                       remote_host, remote_port, transport)
server.serve_forever()

+1,很好的发现。如文档中所述:“有几个实例属性可用...,包括服务器实例self.server,以便[请求处理程序]能够访问每个服务器的信息。” - Air

1

看起来 SocketServer 的 shutdown 方法没有正确关闭 socket。通过下面代码的更改,我保留了对 SocketServer 对象的访问,并直接访问 socket 来关闭它。请注意,socket.close() 在我的情况下可行,但如果其他资源正在访问该 socket,则可能会有人对 socket.shutdown() 进行感兴趣,然后再进行 socket.close()。

[参考:socket.shutdown vs socket.close]

def ThreadTunnel():
    self.t = ForwardServer(('127.0.0.1', 3333), SubHandler)
    self.t.serve_forever()
Thread(target=ThreadTunnel).start()

def stopTunnel(self):
    self.t.shutdown()
    self.trans.close()
    self.c.close()
    self.t.socket.close()

0
你可能希望在生成的线程和调用者之间添加一些同步,以便在通道准备好之前不要尝试使用它。类似这样的东西:
    from threading import Event   
    def startTunnel(self):
        class SubHandler(Handler):
            chain_host = '127.0.0.1'
            chain_port = 5432
            ssh_transport = self.c.get_transport()
        mysignal = Event()
        mysignal.clear()
        def ThreadTunnel():
            global t
            t = ForwardServer(('', 3333), SubHandler)
            mysignal.set() 
            t.serve_forever()
        Thread(target=ThreadTunnel).start()
        mysignal.wait()

0

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