Paramiko SSH 隧道关闭问题

4 投票
4 回答
4383 浏览
提问于 2025-04-16 08:22

我正在写一个Python脚本,想要定期通过已经建立的SSH隧道查询几个远程数据库。我对paramiko库比较熟悉,所以选择了这个库。我希望整个过程都用Python来完成,这样我可以用paramiko处理密钥问题,同时用Python来启动、控制和关闭SSH隧道。

这里有一些相关的问题,但大多数回答似乎都不够完整。下面的解决方案是我目前找到的各种解决方案的拼凑。

现在说说问题:我可以很容易地创建第一个隧道(在一个单独的线程中),并进行我的数据库/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()的方法,但我意识到这段代码并不完全正确,更像是一次实验,试图让隧道正确关闭并测试我的结果。

当我第一次创建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

我想弄清楚为什么这个打开的socket会独立于DBConnect对象而存在,以及如何在我的脚本中正确关闭它。如果我在完全退出Python之前,尝试将不同的连接绑定到不同的IP上,但使用相同的本地端口,就会出现著名的bind err 48地址正在使用中。提前谢谢你们 :)

4 个回答

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()
1

请注意,你不需要像演示代码中那样使用Subhandler的技巧。那个评论是错的。处理器(Handlers)确实可以访问它们服务器的数据。在处理器内部,你可以使用 self.server.instance_data

如果你在你的处理器中使用以下代码,你可以这样使用:

  • self.server.chain_host(链主机)
  • self.server.chain_port(链端口)
  • self.server.ssh_transport(SSH传输)

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

看起来SocketServer的关闭方法没有正确地关闭套接字。通过我下面的代码修改,我可以保留对SocketServer对象的访问,并直接关闭套接字。需要注意的是,在我的情况下,socket.close()可以正常工作,但其他人可能会对先使用socket.shutdown()再用socket.close()感兴趣,特别是当其他资源也在使用这个套接字的时候。

[参考: socket.shutdown与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()

撰写回答